diff --git a/.github/workflows/build-deploy-neurojsonio.yml b/.github/workflows/build-deploy-neurojsonio.yml
new file mode 100644
index 0000000..5d749ff
--- /dev/null
+++ b/.github/workflows/build-deploy-neurojsonio.yml
@@ -0,0 +1,41 @@
+name: Deploy to Production
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ deploy:
+ runs-on: ubuntu-22.04
+ env:
+ CI: false
+
+ steps:
+ - name: Check out the repository
+ uses: actions/checkout@v3
+
+ - name: Install dependencies
+ run: yarn install --frozen-lockfile
+
+ - name: Build React App for production
+ run: |
+ echo "Building for production at root /"
+ PUBLIC_URL="/" yarn build
+
+ - name: Copy JS libraries
+ run: |
+ mkdir -p build/js
+ cp -r public/js/* build/js/
+
+ - name: Deploy to neurojson.io
+ uses: NeuroJSON/SFTP-Deploy-Action@v1.2.5
+ with:
+ server: ${{ secrets.NEUROJ_IO_SERVER }}
+ username: ${{ secrets.NEUROJ_SERVER_USER }}
+ ssh_private_key: ${{ secrets.NEUROJ_SERVER_SSH_KEY }}
+ local_path: "./build/*"
+ remote_path: "${{ secrets.NEUROJ_IO_CI_PATH }}"
diff --git a/.github/workflows/build-deploy-zodiac.yml b/.github/workflows/build-deploy-zodiac.yml
new file mode 100644
index 0000000..ca0bcca
--- /dev/null
+++ b/.github/workflows/build-deploy-zodiac.yml
@@ -0,0 +1,50 @@
+name: Deploy Zodiac Server
+
+on:
+ push:
+ branches:
+ - dev-fan
+ - dev_mario
+ - fangq
+ - staging
+
+jobs:
+ deploy:
+ runs-on: ubuntu-22.04
+ env:
+ CI: false
+
+ steps:
+ - name: Check out the repository
+ uses: actions/checkout@v3
+
+ - name: Install dependencies
+ run: yarn install --frozen-lockfile
+
+ - name: Determine Branch Name
+ id: get_branch
+ run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
+
+ - name: Print GitHub Runner Public IP
+ run: |
+ echo "GitHub runner public IP:"
+ curl https://api.ipify.org
+
+ - name: Build React App with Dynamic PUBLIC_URL
+ run: |
+ echo "Building for /dev/${{ env.BRANCH_NAME }}/"
+ PUBLIC_URL="/dev/${{ env.BRANCH_NAME }}/" yarn build
+
+ - name: Copy JS libraries (jdata, bjdata etc.)
+ run: |
+ mkdir -p build/js
+ cp -r public/js/* build/js/
+
+ - name: Copy package to server
+ uses: NeuroJSON/SFTP-Deploy-Action@v1.2.5
+ with:
+ server: ${{ secrets.NEUROJ_SERVER }}
+ username: ${{ secrets.NEUROJ_SERVER_USER }}
+ ssh_private_key: ${{ secrets.NEUROJ_SERVER_SSH_KEY }}
+ local_path: "./build/*"
+ remote_path: "${{ secrets.NEUROJ_CI_PATH }}/${{ env.BRANCH_NAME }}"
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
new file mode 100644
index 0000000..0ee72f2
--- /dev/null
+++ b/.github/workflows/validate.yml
@@ -0,0 +1,32 @@
+name: Validate NeuroJson_io Project
+
+on:
+ pull_request:
+ branches:
+ - "*"
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out the repository
+ uses: actions/checkout@v3
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: "18"
+
+ - name: Install dependencies
+ run: yarn install
+
+ # - name: Run Linter (Report Issues)
+ # run: yarn lint
+
+ - name: Compile TypeScript
+ run: |
+ PUBLIC_URL="/dev/${{ github.head_ref }}/" yarn build
+
+ - name: Run Security Audit (non-blocking)
+ run: yarn audit --level moderate || echo "Audit failed with moderate issues, continuing anyway."
diff --git a/.gitignore b/.gitignore
index 79fe835..f037b8c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
.env
node_modules
-.DS_Store
\ No newline at end of file
+.DS_Store
+package-lock.json
+
+build
\ No newline at end of file
diff --git a/build-log.txt b/build-log.txt
new file mode 100644
index 0000000..ebdd6e0
--- /dev/null
+++ b/build-log.txt
@@ -0,0 +1,32 @@
+yarn run v1.22.22
+$ craco build --debug
+Creating an optimized production build...
+Compiled with warnings.
+
+Critical dependency: the request of a dependency is an expression
+
+Search for the keywords to learn more about each warning.
+To ignore, add // eslint-disable-next-line to the line before.
+
+File sizes after gzip:
+
+ 717.18 kB build/static/js/main.30f0cad2.js
+
+The bundle size is significantly larger than recommended.
+Consider reducing it with code splitting: https://goo.gl/9VhYWB
+You can also analyze the project dependencies: https://goo.gl/LeUzfb
+
+The project was built assuming it is hosted at /.
+You can control this with the homepage field in your package.json.
+
+The build folder is ready to be deployed.
+You may serve it with a static server:
+
+ yarn global add serve
+ serve -s build
+
+Find out more about deployment here:
+
+ https://cra.link/deployment
+
+Done in 15.95s.
diff --git a/craco.config.js b/craco.config.js
new file mode 100644
index 0000000..45e636d
--- /dev/null
+++ b/craco.config.js
@@ -0,0 +1,21 @@
+module.exports = {
+ webpack: {
+ configure: (webpackConfig) => {
+ webpackConfig.resolve.fallback = {
+ ...webpackConfig.resolve.fallback,
+ path: require.resolve("path-browserify"),
+ };
+
+ webpackConfig.module.rules.push({
+ test: /\.js$/,
+ parser: {
+ requireEnsure: false, // Prevents Webpack from treating require() as a critical dependency
+ },
+ });
+
+ webpackConfig.ignoreWarnings = [/the request of a dependency is an expression/];
+
+ return webpackConfig;
+ },
+ },
+};
diff --git a/package.json b/package.json
index 1fb4904..b711c2f 100644
--- a/package.json
+++ b/package.json
@@ -4,35 +4,61 @@
"private": true,
"dependencies": {
"3d-force-graph": "^1.73.4",
+ "@babel/helpers": "7.26.10",
+ "@babel/runtime": "7.26.10",
+ "@craco/craco": "^7.1.0",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.3",
"@mui/material": "^5.14.4",
"@reduxjs/toolkit": "^1.9.5",
+ "@rjsf/core": "^5.24.8",
+ "@rjsf/mui": "^5.24.8",
+ "@rjsf/utils": "^5.24.8",
+ "@rjsf/validator-ajv8": "^5.24.8",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
"@types/jest": "^27.0.1",
+ "@types/numjs": "^0.16.8",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
- "@types/three": "^0.169.0",
+ "@types/three": "^0.176.0",
+ "ajv": "^8",
+ "ajv-keywords": "^5",
"axios": "^1.4.0",
+ "bda": "^1.0.0",
+ "bjd": "^0.3.2",
+ "buffer": "6.0.3",
"dayjs": "^1.11.10",
+ "jquery": "^3.7.1",
+ "json-stringify-safe": "^5.0.1",
"jwt-decode": "^3.1.2",
+ "lzma": "^2.3.2",
+ "numjs": "^0.16.1",
+ "pako": "1.0.11",
+ "path-browserify": "^1.0.1",
"query-string": "^8.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-json-view": "^1.21.3",
"react-redux": "^8.1.2",
"react-router-dom": "^6.15.0",
- "react-scripts": "5.0.1",
- "three": "^0.169.0",
+ "react-scripts": "^5.0.1",
+ "react-syntax-highlighter": "^15.6.1",
+ "sharp": "^0.33.5",
+ "stats-js": "^1.0.1",
+ "stats.js": "0.17.0",
+ "three": "0.145.0",
"typescript": "^5.1.6",
+ "uplot": "1.6.17",
"web-vitals": "^2.1.0"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/node": "^20.5.7",
+ "@types/pako": "^2.0.3",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
"eslint": "^8.21.0",
@@ -46,10 +72,11 @@
"prettier": "^2.3.1"
},
"scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build",
- "test": "react-scripts test",
- "eject": "react-scripts eject"
+ "start": "craco start || react-scripts start",
+ "build": "craco build || react-scripts build",
+ "test": "craco test || react-scripts test",
+ "eject": "craco eject || react-scripts eject",
+ "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}' --ext .js,.jsx,.ts,.tsx"
},
"browserslist": {
"production": [
@@ -62,5 +89,11 @@
"last 1 firefox version",
"last 1 safari version"
]
+ },
+ "resolutions": {
+ "postcss": "^8.4.31",
+ "nth-check": "^2.0.1",
+ "@babel/runtime": "7.26.10",
+ "3d-force-graph": "1.74.6"
}
}
diff --git a/public/.htaccess b/public/.htaccess
new file mode 100644
index 0000000..4d81dbc
--- /dev/null
+++ b/public/.htaccess
@@ -0,0 +1,9 @@
+RedirectMatch 404 /\.git
+
+ RewriteEngine On
+ RewriteBase /
+ RewriteCond %{REQUEST_FILENAME} -f [OR]
+ RewriteCond %{REQUEST_FILENAME} -d
+ RewriteRule ^ - [L]
+ RewriteRule ^ index.html [L]
+
diff --git a/public/img/3d_graph_logo.png b/public/img/3d_graph_logo.png
new file mode 100644
index 0000000..7a7a637
Binary files /dev/null and b/public/img/3d_graph_logo.png differ
diff --git a/public/img/about_page/api.png b/public/img/about_page/api.png
new file mode 100644
index 0000000..53168fc
Binary files /dev/null and b/public/img/about_page/api.png differ
diff --git a/public/img/about_page/download.png b/public/img/about_page/download.png
new file mode 100644
index 0000000..a5534b4
Binary files /dev/null and b/public/img/about_page/download.png differ
diff --git a/public/img/about_page/preview.png b/public/img/about_page/preview.png
new file mode 100644
index 0000000..2dcf2f5
Binary files /dev/null and b/public/img/about_page/preview.png differ
diff --git a/public/img/about_page/search.png b/public/img/about_page/search.png
new file mode 100644
index 0000000..47bd2e2
Binary files /dev/null and b/public/img/about_page/search.png differ
diff --git a/public/img/about_page/search_icon.png b/public/img/about_page/search_icon.png
new file mode 100644
index 0000000..672822e
Binary files /dev/null and b/public/img/about_page/search_icon.png differ
diff --git a/public/io_fav.png b/public/img/io_fav.png
similarity index 100%
rename from public/io_fav.png
rename to public/img/io_fav.png
diff --git a/public/img/logo_updated.png b/public/img/logo_updated.png
new file mode 100644
index 0000000..248f22e
Binary files /dev/null and b/public/img/logo_updated.png differ
diff --git a/public/img/logo_yellow.png b/public/img/logo_yellow.png
new file mode 100644
index 0000000..776bd2d
Binary files /dev/null and b/public/img/logo_yellow.png differ
diff --git a/public/img/neurojson_logo_color.png b/public/img/neurojson_logo_color.png
new file mode 100644
index 0000000..f4c5170
Binary files /dev/null and b/public/img/neurojson_logo_color.png differ
diff --git a/public/img/section3_cards.png b/public/img/section3_cards.png
new file mode 100644
index 0000000..0631d9a
Binary files /dev/null and b/public/img/section3_cards.png differ
diff --git a/public/img/section4/click_icon.png b/public/img/section4/click_icon.png
new file mode 100644
index 0000000..83b07a4
Binary files /dev/null and b/public/img/section4/click_icon.png differ
diff --git a/public/img/section4/workflow1.png b/public/img/section4/workflow1.png
new file mode 100644
index 0000000..b1ccdf7
Binary files /dev/null and b/public/img/section4/workflow1.png differ
diff --git a/public/img/section4/workflow2.png b/public/img/section4/workflow2.png
new file mode 100644
index 0000000..c071741
Binary files /dev/null and b/public/img/section4/workflow2.png differ
diff --git a/public/img/section4/workflow3.png b/public/img/section4/workflow3.png
new file mode 100644
index 0000000..bb7db49
Binary files /dev/null and b/public/img/section4/workflow3.png differ
diff --git a/public/img/section4/workflow4.png b/public/img/section4/workflow4.png
new file mode 100644
index 0000000..a5e344a
Binary files /dev/null and b/public/img/section4/workflow4.png differ
diff --git a/public/img/static_nodes.png b/public/img/static_nodes.png
new file mode 100644
index 0000000..061c066
Binary files /dev/null and b/public/img/static_nodes.png differ
diff --git a/public/index.html b/public/index.html
index 40bc154..87030c6 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,18 +1,15 @@
-
+
-
+
-
+
+
+
-
+
+
+
+
NeuroJSON.io - Free Data Worth Sharing
+
+
@@ -53,5 +97,35 @@
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/js/bjdata.js b/public/js/bjdata.js
new file mode 100644
index 0000000..8c3a444
--- /dev/null
+++ b/public/js/bjdata.js
@@ -0,0 +1,695 @@
+// Licensed to Pioneers in Engineering under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. Pioneers in Engineering licenses
+// this file to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License
+
+// This file was (hastily) ported from lua-ubjson.
+// The code structure there will make probably make more sense.
+// Multiple return values there have been manually transformed into arrays
+// here, and generally make the code harder to understand.
+// There are also a few lingering Lua-isms, like keeping track of stack depth
+// for error handling, excessive use of nil / null, variable names suggesting
+// string and buffers are the same thing, and ambiguity about what's an array
+// and what's an object.
+// The comments also have not been updated. Comments that look like they've
+// been mangled by a regex probably have.
+
+// Global dependencies here.
+
+//var buffer_module = require('buffer');
+
+var abs = Math.abs;
+var floor = Math.floor;
+var dumpint = function (val, size, type, endian) {
+ var b = buffer_module.Buffer(size);
+ if (size === 1)
+ endian = '';
+ b[write_fun[type] + endian].apply(b, [val, 0]);
+ return b;
+};
+
+var dumpfloat = function (val, type, endian) {
+ var b;
+
+ if (type === 'd') {
+ b = buffer_module.Buffer(4);
+ b['writeFloat' + endian].apply(b, [val, 0]);
+ } else if (type === 'D') {
+ b = buffer_module.Buffer(8);
+ b['writeDouble' + endian].apply(b, [val, 0]);
+ } else {
+ throw 'Unexpected float type ' + type + '.';
+ }
+ return b;
+};
+
+var undumpint = function(buf, offset, type, size, endian) {
+ if (size === 1)
+ endian = '';
+ return buf[read_fun[type] + endian].apply(buf, [offset]);
+};
+
+var undumpfloat = function(buf, offset, type, endian) {
+ if (type === 'd') {
+ return buf['readFloat' + endian].apply(buf, [offset]);
+ } else if (type === 'D') {
+ return buf['readDouble' + endian].apply(buf, [offset]);
+ } else {
+ throw 'Unexpected float type ' + type + '.';
+ }
+};
+
+var toBuffer = function (str) {
+ return buffer_module.Buffer(str);
+};
+
+var type = function (val) {
+ return typeof val;
+};
+var error = function (msg) {
+ throw "js-bjdata: " + msg;
+};
+
+var insert = function(array, val) {
+ array.push(buffer_module.Buffer(val));
+};
+
+var bufStr = function(buf, start, end) {
+ return buf.slice(start, end + 1).toString();
+};
+
+// Mapping from maximum value -> ubjson tag
+var int_maxes = [
+ 256.0,
+ 65536.0,
+ 4294967296.0,
+ 18446744073709551616.0,
+];
+
+var int_tags = [
+ 'U',
+ 'u',
+ 'm',
+ 'M',
+ 'M'
+];
+
+var neg_int_tags = [
+ 'i',
+ 'I',
+ 'l',
+ 'L',
+ 'L'
+];
+
+// ubjson tag -> size in bytes
+var int_tag_size = {
+ U : 1,
+ i : 1,
+ u : 2,
+ I : 2,
+ m : 4,
+ l : 4,
+ M : 8,
+ L : 8,
+};
+
+// bjdata tag -> jdata tags
+var jd_type = {
+ U : "uint8",
+ i : "int8",
+ u : "uint16",
+ I : "int16",
+ m : "uint32",
+ l : "int32",
+ M : "uint64",
+ L : "int64",
+ h : "int16",
+ d : "float32",
+ D : "float64",
+};
+
+var write_fun = {
+ U : "writeUInt8",
+ i : "writeInt8",
+ u : "writeUInt16",
+ I : "writeInt16",
+ m : "writeUInt32",
+ l : "writeInt32",
+ M : "writeBigUInt64",
+ L : "writeBigInt64",
+ h : "writeInt16",
+ d : "writeFloat",
+ D : "writeDouble",
+ C : "writeUInt8"
+};
+
+var read_fun = {
+ U : "readUInt8",
+ i : "readInt8",
+ u : "readUInt16",
+ I : "readInt16",
+ m : "readUInt32",
+ l : "readInt32",
+ M : "readBigUInt64",
+ L : "readBigInt64",
+ h : "readInt16",
+ d : "readFloat",
+ D : "readDouble",
+ C : "readUInt8"
+};
+
+// bjdata tag -> jdata tags
+var jd_len = {
+ U : 1,
+ i : 1,
+ u : 2,
+ I : 2,
+ m : 4,
+ l : 4,
+ M : 8,
+ L : 8,
+ d : 4,
+ D : 8,
+};
+
+var typedfun={
+ "Float32Array":null,"Float64Array":null,
+ "Int8Array":null, "Uint8Array":null,
+ "Int16Array":null, "Uint16Array":null,
+ "Int32Array":null, "Uint32Array":null,
+ "BigInt64Array":null, "BigUint64Array":null
+};
+
+// Use doubles to serialize Lua numbers.
+var use_double = true;
+
+// Get the smallest tag and size to hold a value.
+function int_tag(val) {
+ if ( val >= 0 && val < 256 ) {
+ return ['U', 1];
+ }
+ var last_key = 'i';
+ // Calculate the absolute value.
+ if ( val < 0 ) {
+ for (let idx = 0 ; idx < int_maxes.length; idx++) {
+ let max = int_maxes[idx];
+ if ( val >= -max/2 ) {
+ return [last_key, int_tag_size[last_key]];
+ } else {
+ last_key = neg_int_tags[idx+1];
+ }
+ }
+ }
+ last_key = 'U';
+ for (let idx = 0 ; idx < int_maxes.length; idx++) {
+ let max = int_maxes[idx];
+ if ( val < max ) {
+ return [last_key, int_tag_size[last_key]];
+ } else {
+ last_key = int_tags[idx+1];
+ }
+ }
+ return [last_key, int_tag_size[last_key]];
+}
+
+// If val can be represented by a fixed size value type, return the tag for
+// that type, otherwise return the Lua type string.
+function val_tag(val) {
+ var t = type(val);
+ if ( t === 'number' || t === 'bigint') {
+ t = int_tag(val)[0];
+ } else if ( t === 'boolean' ) {
+ if ( t ) {
+ return 'T';
+ } else {
+ return 'F';
+ }
+ } else if ( t === 'null' ) {
+ return 'Z';
+ }
+ return t;
+}
+
+// Pre-declare encode_inner
+var encode_inner;
+
+// Determines whether an table should become an array or a table.
+// Also determines length, and whether the optimized container optimization can
+// be applied.
+// returns [use_obj, length, max_index, shared_tag, write_val]
+// where
+// use_obj is true iff the table should become a ubjson object (not an array).
+// length is the number of entries in the array or table.
+// max_index is the largest integer index.
+// shared_tag is a ubjson type tag if ( the optimized container format can be
+// applied, otherwise is the string 'mixed'
+// write_val is a function which writes a value for the object or array.
+// write_val has the same type as encode_inner
+// (value, buffer, memo, depth) -> ()
+// where value is the value to be serialized
+// buffer is a table of strings which will be concatenated together to
+// produce the output
+// memo is a table mapping currently entered tables to true
+// depth is the recursion depth from the user's call
+function array_helper(val) {
+ // TODO(kzentner): Handle integer tags more intelligently.
+ // Currently, this function only handles integers well when every integer is
+ // expected to have the same tag. In theory, this could produce an array for
+ // any integer type tag, but in practice probably will almost always only
+ // produce 'U'.
+ // Basically, this function expects val_tag to return the same tag for every
+ // value if the fixed-type container optimization can be applied. This is
+ // definitely not true. For example, [0, -1, 2] produces the tags ['U', 'i',
+ // 'U'], but all the entries can be represented by one signed byte.
+ //
+ var t = null;
+ var length = 0;
+ var max = 0;
+
+ for (var k in val) {
+ var v = val[k];
+ if ( k > max ) {
+ max = k;
+ }
+ if ( t === null ) {
+ t = val_tag(v);
+ }
+ if ( t !== val_tag(v) ) {
+ t = 'mixed';
+ }
+ length = length + 1;
+ }
+
+ var write_val = encode_inner;
+
+ if ( t !== null && t.length === 1 ) {
+ var size = int_tag_size[t];
+ if ( size ) {
+ write_val = function(val, buffer, memo) {
+ insert(buffer, dumpint(val, size, t, 'LE'));
+ };
+ } else if ( t === 'd' ) {
+ write_val = function(val, buffer, memo) {
+ insert(buffer, dumpfloat(val, 'd', 'LE'));
+ };
+ } else if ( t === 'D' ) {
+ write_val = function(val, buffer, memo) {
+ insert(buffer, dumpfloat(val, 'D', 'LE'));
+ };
+ } else {
+ // Tag should be 'T', 'F', 'Z'
+ write_val = function(val, buffer, memo) {
+ };
+ }
+ }
+
+ // TODO(kzentner): Handle array-likes like Uint8Array's, etc. better.
+ // Note that isArray(new Uint8Array) == false
+ return [!Array.isArray(val), length, max, t, write_val];
+}
+
+function encode_int(val, buffer) {
+ var ts = int_tag(val);
+ var tag = ts[0];
+ var size = ts[1];
+ insert(buffer, tag);
+ insert(buffer, dumpint(val, size, tag, 'LE'));
+}
+
+function encode_inner(val, buffer, memo, depth) {
+ var k;
+ // if val in memo. Some things, Javascript makes really weird.
+ if ( ~memo.indexOf(val) ) {
+ error('Cannot serialize circular data structure.', depth);
+ }
+ if ( depth === undefined ) {
+ error('Depth missing.');
+ }
+
+ var t = type(val);
+ if ( t === 'number' ) {
+ if ( floor(val) === val ) {
+ encode_int(val, buffer);
+ } else {
+ if ( use_double ) {
+ insert(buffer, 'D');
+ insert(buffer, dumpfloat(val, 'D', 'LE'));
+ } else {
+ insert(buffer, 'd');
+ insert(buffer, dumpfloat(val, 'd', 'LE'));
+ }
+ }
+ } else if (t === 'bigint') {
+ encode_int(val, buffer);
+ } else if ( t === 'null' || t === 'undefined' ) {
+ insert(buffer, 'Z');
+ } else if ( t === 'boolean' ) {
+ if ( val ) {
+ insert(buffer, 'T');
+ } else {
+ insert(buffer, 'F');
+ }
+ } else if ( t === 'string' ) {
+ insert(buffer, 'S');
+ encode_int(val.length, buffer);
+ insert(buffer, val);
+ } else if ( t === 'object' ) {
+ memo.push(val);
+ var ulmtw = array_helper(val);
+ var use_obj = ulmtw[0];
+ var length = ulmtw[1];
+ var max = ulmtw[2];
+ var tag = ulmtw[3];
+ var write_val = ulmtw[4];
+ if ( use_obj ) {
+ insert(buffer, '{');
+ } else {
+ insert(buffer, '[');
+ }
+
+ if ( !use_obj && tag !== null && tag.length === 1 ) {
+ insert(buffer, '$');
+ insert(buffer, tag);
+ insert(buffer, '#');
+ encode_int(length, buffer);
+ }
+
+ if ( use_obj ) {
+ for (k in val) {
+ var v = val[k];
+ var str = k + '';
+ encode_int(str.length, buffer);
+ insert(buffer, str);
+ encode_inner(v, buffer, memo, depth + 1);
+ }
+ } else {
+ if(tag !== null) {
+ for (k = 0; k <= max; k++ ) {
+ write_val(val[k], buffer, memo, depth + 1);
+ }
+ } else {
+ for (k = 0; k <= max; k++ ) {
+ encode_inner(val[k], buffer, memo, depth + 1);
+ }
+ }
+ }
+
+ if ( use_obj || tag === null ) {
+ if ( use_obj ) {
+ insert(buffer, '}');
+ } else {
+ insert(buffer, ']');
+ }
+ }
+
+ // Remove val from memo.
+ memo.splice(memo.indexOf(val), 1);
+ }
+}
+
+function encode(value, state) {
+ var buffer = [];
+ var memo = [];
+ var k;
+ encode_inner(value, buffer, memo, 3);
+ var total_length = 0;
+ for (k in buffer) {
+ total_length += buffer[k].length;
+ }
+ var out = buffer_module.Buffer(total_length);
+ var current_offset = 0;
+ for (k in buffer) {
+ var b = buffer[k];
+ b.copy(out, current_offset, 0, b.length);
+ current_offset += b.length;
+ }
+ return out;
+}
+
+function decode_int(str, offset, depth, error_context) {
+ var c = bufStr(str, offset, offset);
+ var int_size = int_tag_size[c];
+ if ( int_size === undefined ) {
+ error(error_context + ' length did not have an integer tag.', depth);
+ }
+ var i = undumpint(str, offset + 1, c, int_size, 'LE');
+ if ( c === 'U' && i < 0 ) {
+ // Undo twos-complement
+ i = 256 + i;
+ }
+
+ return [i, offset + 1 + int_size];
+}
+
+// Returns function with signature
+// (str, offset, depth) -> val, new_offset, skip
+// where str is the input string
+// offset is the index into str to start reading at
+// depth is the recursion depth from the user's call
+// val is the read value
+// new_offset is the offset after the read element
+// skip is whether the object should be recognized
+// (used to implement noop)
+function get_read_func(tag) {
+ var int_size = int_tag_size[tag];
+ if ( tag === 'C' ) {
+ int_size = 1;
+ }
+ if ( int_size !== undefined ) {
+ return function(str, offset, depth) {
+ return [undumpint(str, offset, tag, int_size, 'LE'), offset + int_size];
+ };
+ } else if ( tag === 'd' ) {
+ return function(str, offset, depth) {
+ return [undumpfloat(str, offset, 'd', 'LE'), offset + 4];
+ };
+ } else if ( tag === 'D' ) {
+ return function(str, offset, depth) {
+ return [undumpfloat(str, offset, 'D', 'LE'), offset + 8];
+ };
+ } else if ( tag === 'T' ) {
+ return function(str, offset, depth) {
+ return [true, offset];
+ };
+ } else if ( tag === 'F' ) {
+ return function(str, offset, depth) {
+ return [false, offset];
+ };
+ } else if ( tag === 'Z' ) {
+ return function(str, offset, depth) {
+ return [null, offset];
+ };
+ } else if ( tag === 'N' ) {
+ return function(str, offset, depth) {
+ return [null, offset, true];
+ };
+ } else {
+ return null;
+ }
+}
+
+// Decodes a string. Does ! read the type tag, so that it can be used to
+// decode ubjson object keys.
+function decode_str(str, offset, depth) {
+ var ls = decode_int(str, offset, depth + 1, 'String at offset ' + offset);
+ var str_length = ls[0];
+ var str_start = ls[1];
+ // Since bufStr is inclusive at of the end, -1 is needed.
+ return [bufStr(str, str_start, str_start + str_length - 1), str_start + str_length];
+}
+
+// Recursive function used to decode object.
+// (str, offset, depth) -> (val, new_offset, skip)
+// where str is the input string
+// offset is the index into str to start reading at
+// depth is the recursion depth from the user's call
+// val is the read value
+// new_offset is the offset after the read element
+// skip is whether the object should be recognized
+// (used to implement noop)
+function decode_inner(str, offset, depth) {
+ if ( depth === null ) {
+ error('Depth missing');
+ }
+ var c = bufStr(str, offset, offset);
+ var int_size = int_tag_size[c];
+ if ( int_size !== undefined ) {
+ return [undumpint(str, offset + 1, c, int_size, 'LE'), offset + 1 + int_size];
+ } else if ( c === 'C' ) {
+ return [undumpint(str, offset + 1, c, 1, 'LE'), offset + 2];
+ } else if ( c === 'S' || c === 'H' ) {
+ // TODO(kzentner): How to handle huge numbers?
+ return decode_str(str, offset + 1, depth + 1)
+ } else if ( c === 'T' ) {
+ return [true, offset + 1];
+ } else if ( c === 'F' ) {
+ return [false, offset + 1];
+ } else if ( c === 'Z' ) {
+ return [null, offset + 1];
+ } else if ( c === 'N' ) {
+ return [null, offset + 1, true];
+ } else if ( c === 'd' ) {
+ return [undumpfloat(str, offset + 1, 'd', 'LE'), offset + 5];
+ } else if ( c === 'D' ) {
+ return [undumpfloat(str, offset + 1, 'D', 'LE'), offset + 9];
+ } else if ( c === '[' || c === '{' ) {
+ var start_offset = offset + 1;
+ var tag = bufStr(str, start_offset, start_offset);
+ var length = null;
+ var out;
+ var read_val = decode_inner;
+ var t = ' '
+ if ( tag === '$' ) {
+ start_offset = start_offset + 1;
+ t = bufStr(str, start_offset, start_offset);
+ start_offset = start_offset + 1;
+ tag = bufStr(str, start_offset, start_offset);
+ read_val = get_read_func(t);
+ if ( read_val === null ) {
+ if ( c === '[' ) {
+ error('Type tag for non value type in array at offset ' + offset,
+ depth);
+ } else {
+ error('Type tag for non value type in object at offset ' + offset,
+ depth);
+ }
+ }
+ }
+
+ // TODO(kzentner): Do not construct the error message every time.
+ if ( tag === '#' ) {
+ var msg;
+ if ( c === '[' ) {
+ msg = 'Array';
+ } else {
+ msg = 'Object';
+ }
+ msg = msg + ' length at offset ' + offset;
+ var ls;
+ start_offset = start_offset + 1;
+
+ if(bufStr(str, start_offset, start_offset) == '['){
+ ls = decode_inner(str, start_offset, depth + 1, msg);
+ length = ls[0].reduce((a, b)=> a*b, 1);
+ }else{
+ ls = decode_int(str, start_offset, depth + 1, msg);
+ length = ls[0];
+ }
+ start_offset = ls[1];
+ }
+ var elt_offset = start_offset;
+ var key, val, skip;
+ var ke;
+ var ves;
+ var i;
+ if ( c === '[' ) {
+ out = [];
+ if ( length !== null ) {
+ elt_offset = start_offset;
+ let tagid=jd_type[t];
+ if(tagid !== undefined){
+ let type=jd_type[t];
+ let bytelen=jd_len[t] * length;
+ let typename=type.charAt(0).toUpperCase() + type.substring(1) + "Array";
+ if(type=='int64' || type=='uint64')
+ typename='Big'+typename;
+ out=new Uint8Array(buffer_module.Buffer.from(str.buffer, elt_offset, bytelen));
+ if(typedfun[typename] == null)
+ typedfun[typename]=new Function('d', 'return new '+typename+'(d)');
+ let typecast=typedfun[typename];
+ out=typecast(out.buffer);
+ elt_offset+=bytelen;
+ }else{
+ for (i = 0; i < length; i++) {
+ ves = read_val(str, elt_offset, depth + 1);
+ val = ves[0];
+ elt_offset = ves[1];
+ skip = ves[2];
+ if ( ! skip ) {
+ out.push(val);
+ }
+ }
+ }
+ } else {
+ while ( bufStr(str, elt_offset, elt_offset) !== ']' ) {
+ ves = read_val(str, elt_offset, depth + 1);
+ val = ves[0];
+ elt_offset = ves[1];
+ skip = ves[2];
+ if ( ! skip ) {
+ out.push(val);
+ }
+ }
+ elt_offset++;
+ }
+ } else {
+ out = {};
+ if ( length !== null ) {
+ for (i = 0; i < length; i++) {
+ ke = decode_str(str, elt_offset, depth + 1);
+ key = ke[0];
+ elt_offset = ke[1];
+ ves = read_val(str, elt_offset, depth + 1);
+ val = ves[0];
+ elt_offset = ves[1];
+ skip = ves[2];
+ if ( ! skip ) {
+ out[key] = val;
+ }
+ }
+ } else {
+ while ( bufStr(str, elt_offset, elt_offset) !== '}' ) {
+ ke = decode_str(str, elt_offset, depth + 1);
+ key = ke[0];
+ elt_offset = ke[1];
+ ves = read_val(str, elt_offset, depth + 1);
+ val = ves[0];
+ elt_offset = ves[1];
+ skip = ves[2];
+ if ( ! skip ) {
+ out[key] = val;
+ }
+ }
+ elt_offset++;
+ }
+ }
+ return [out, elt_offset];
+ } else {
+ error('Unrecognized type tag ' + c + ' at offset ' + offset + '.', depth);
+ }
+}
+
+// Get decoded value and the offset after it in the buffer.
+function decode_offset(str, offset) {
+ if (offset === undefined) {
+ offset = 0;
+ }
+ return decode_inner(str, offset, 1);
+}
+
+// Just get the decoded value.
+function decode(str, offset) {
+ return decode_offset(str, offset);
+}
+
+var bjdata = {
+ version : 'js-bjdata 0.2',
+ encode : encode,
+ decode : decode,
+ decode_offset: decode_offset
+};
+
+//module.exports = bjdata;
\ No newline at end of file
diff --git a/public/js/jdata.js b/public/js/jdata.js
new file mode 100644
index 0000000..5314629
--- /dev/null
+++ b/public/js/jdata.js
@@ -0,0 +1,291 @@
+/********************************************************************************
+ JSData - Lightweight JData Annotation Encoder and Decoder for JavaScript and NodeJS
+ (for JData Specification Draft 2 defined in http://neurojson.org/jdata/draft2)
+
+ Author: Qianqian Fang
+ URL: http://github.com/NeuroJSON/jsdata
+ Live Demo: https://jsfiddle.net/fangq/7vLxjwa6/
+********************************************************************************/
+
+class jdata {
+
+ constructor(data = {}, options = {}) {
+ this.opt = options;
+ this.data = data;
+ this.const = {
+ _Inf_: Infinity,
+ _NaN_: NaN
+ };
+ this.base64 = options.hasOwnProperty('base64') ? options.base64 : true;
+ this.typedfun = {
+ "Float32Array": null,
+ "Float64Array": null,
+ "Int8Array": null,
+ "Uint8Array": null,
+ "Int16Array": null,
+ "Uint16Array": null,
+ "Int32Array": null,
+ "Uint32Array": null,
+ "BigInt64Array": null,
+ "BigUint64Array": null
+ };
+ this._zipper = (typeof pako !== 'undefined' ?
+ pako :
+ options.hasOwnProperty('zlib') ? require(options['zlib']) : require('pako'));
+ this._nj = (typeof nj !== 'undefined' ?
+ nj :
+ require('numjs'));
+ this._nj.NdArray.prototype.toJSON = function() {
+ return JSON.stringify(this.tolist(), function(k, v) {
+ if (typeof v === 'bigint')
+ return '~~' + v.toString() + '~~';
+ return v;
+ });
+ };
+ }
+
+ encode() {
+ this.data = this._encode(this.data);
+ return this;
+ }
+
+ decode() {
+ this.data = this._decode(this.data);
+ return this;
+ }
+
+ base64ToBytes(base64) {
+ const binString = atob(base64);
+ return Uint8Array.from(binString, (m) => m.codePointAt(0));
+ }
+
+ bytesToBase64(bytes) {
+ const binString = String.fromCodePoint(...bytes);
+ return btoa(binString);
+ }
+
+ tojson() {
+ return JSON.stringify(this.data, this._exportfilter.bind(this), '\t').replace(/\\/g, '')
+ .replace(/\"\[/g, '[')
+ .replace(/\]\"/g, ']')
+ .replace(/\"\{/g, '{')
+ .replace(/\}\"/g, '}')
+ .replace(/\"~~/g, '')
+ .replace(/~~\"/g, '');
+ }
+
+ zip(buf, method) {
+ if (method !== 'zlib' || method !== 'gzip')
+ method = 'zlib';
+ if (method === 'zlib') {
+ if (this.base64)
+ return this.bytesToBase64(this._zipper.deflate(new Uint8Array(buf), {
+ to: 'string'
+ }));
+ else
+ return this._zipper.deflate(new Uint8Array(buf), {
+ to: 'string'
+ });
+ } else if (method === 'gzip') {
+ if (this.base64)
+ return this.bytesToBase64(this._zipper.gzip(new Uint8Array(buf), {
+ to: 'string'
+ }));
+ else
+ return this._zipper.gzip(new Uint8Array(buf), {
+ to: 'string'
+ });
+ }
+ }
+
+ unzip(str, method) {
+ if (method === 'zlib')
+ return this._zipper.inflate(str);
+ else if (method === 'gzip')
+ return this._zipper.ungzip(str);
+ else if (method === 'lzma') {
+ if (typeof LZMA !== 'undefined') { // need js-lzma (https://github.com/jcmellado/js-lzma)
+ var inStream = new LZMA.iStream(str.buffer);
+ return LZMA.decompressFile(inStream).toUint8Array();
+ }
+ return this._zipper.decompressFile(str); // must use {zlib:'lzma-purejs'} in the constructor
+ } else
+ throw "compression method not supported";
+ }
+
+ _istypedarray(obj) {
+ return !!obj && obj.byteLength !== undefined;
+ }
+
+ static _str2hex(str) {
+ str = encodeURIComponent(str).split('%').join('');
+ return str.toLowerCase();
+ }
+
+ _exportfilter(k, v) {
+ if (typeof v === 'bigint') {
+ return v.toString();
+ } else if (v instanceof Array) {
+ return JSON.stringify(v);
+ } else if (this._istypedarray(v)) {
+ return Array.apply([], v);
+ } else if (v instanceof this._nj.NdArray) {
+ return v.tolist();
+ } else if (v instanceof Map)
+ return Array.from(v.entries());
+ return v;
+ }
+
+ _encode(obj) {
+ let newobj = obj;
+ if (typeof obj == 'number') {
+ if (obj === Infinity) {
+ return "_Inf_";
+ } else if (obj === -Infinity) {
+ return "-_Inf_";
+ } else if (obj !== obj) {
+ return "_NaN_";
+ }
+ } else if (obj instanceof Array) {
+ obj.forEach(function(e, idx, orig) {
+ orig[idx] = this._encode(e);
+ }.bind(this));
+ newobj = obj;
+ } else if (this._istypedarray(obj)) {
+ let dtype = Object.prototype.toString.call(obj);
+ if (dtype == '[object ArrayBuffer]') {
+ obj = new Uint8Array(obj);
+ dtype = Object.prototype.toString.call(obj);
+ }
+ dtype = dtype.replace(/\[object /, '').replace(/^Big/, '')
+ .replace(/Array\]/, '').replace(/Clamped/, '');
+ dtype = dtype.replace(/Float32/, 'single').replace(/Float64/, 'double');
+ newobj = {
+ _ArrayType_: dtype.toLowerCase(),
+ _ArraySize_: obj.length
+ };
+ if ((dtype == 'Int64' || dtype == 'Uint64') || this.opt !== undefined && this.opt.hasOwnProperty('compression')) {
+ if (this.opt.compression === undefined)
+ newobj._ArrayZipType_ = 'zlib';
+ else
+ newobj._ArrayZipType_ = this.opt.compression;
+ newobj._ArrayZipSize_ = obj.length;
+ newobj._ArrayZipData_ = this.zip(obj.buffer, newobj._ArrayZipType_);
+ } else {
+ newobj._ArrayData_ = Array.from(obj);
+ }
+ } else if (typeof obj === 'bigint') {
+ newobj = this._encode(BigInt64Array.from([obj]));
+ } else if (obj instanceof this._nj.NdArray) {
+ let dtype = obj.dtype;
+ dtype = dtype.replace(/float32/, 'single').replace(/float64/, 'double');
+ newobj = {
+ _ArrayType_: dtype.toLowerCase(),
+ _ArraySize_: obj.shape
+ };
+ if (this.opt !== undefined && this.opt.hasOwnProperty('compression')) {
+ newobj._ArrayZipType_ = this.opt.compression;
+ newobj._ArrayZipSize_ = [1, obj.size];
+ newobj._ArrayZipData_ = this.zip(obj.selection.data.buffer, this.opt.compression);
+ } else {
+ newobj._ArrayData_ = obj.flatten().tolist();
+ }
+ } else if (obj instanceof Map) {
+ newobj = {
+ _MapData_: []
+ };
+ obj.forEach(function(value, key) {
+ newobj._MapData_.push([this._encode(key), this._encode(value)]);
+ }.bind(this));
+ } else if (typeof obj == 'object' && obj !== null) {
+ for (var k in obj) {
+ newobj[k] = this._encode(obj[k]);
+ }
+ }
+ return newobj;
+ }
+
+ _decode(obj) {
+ let newobj = obj;
+ if (obj instanceof Array) {
+ obj.forEach(function(e, idx, orig) {
+ orig[idx] = this._decode(e);
+ }.bind(this));
+ newobj = obj;
+ } else if ((typeof obj == 'string') && obj.length <= 6 && obj.slice(-1) == '_') {
+ if (obj == '_Inf_')
+ newobj = Infinity;
+ else if (obj == '-_Inf_')
+ newobj = -Infinity;
+ else if (obj == '_NaN_')
+ newobj = NaN;
+ } else if (typeof obj == 'object' && obj !== null) {
+ if (obj.hasOwnProperty('_ArrayType_')) {
+ let type = obj._ArrayType_;
+ let data;
+ type = type.replace(/single/, 'float32').replace(/double/, 'float64');
+ let typename = type.charAt(0).toUpperCase() + type.substring(1) + "Array";
+ if (type == 'int64' || type == 'uint64')
+ typename = 'Big' + typename;
+ if (obj.hasOwnProperty('_ArrayZipData_')) {
+ if (this.base64)
+ data = this.unzip(this.base64ToBytes(obj._ArrayZipData_), obj._ArrayZipType_);
+ else
+ data = this.unzip(obj._ArrayZipData_, obj._ArrayZipType_);
+ //data=Uint8Array.from(data);
+ if (this.typedfun[typename] == null)
+ this.typedfun[typename] = new Function('d', 'return new ' + typename + '(d)');
+ let typecast = this.typedfun[typename];
+ data = typecast(data.buffer);
+ data = this._nj.array(data, type).reshape(obj._ArraySize_);
+ } else if (obj.hasOwnProperty('_ArrayData_')) {
+ data = obj._ArrayData_;
+ }
+ newobj = data;
+ return newobj;
+ } else if (obj.hasOwnProperty('_MapData_') && Array.isArray(obj._MapData_)) {
+ newobj = new Map();
+ obj._MapData_.forEach(function(e) {
+ newobj.set(this._decode(e[0]), this._decode(e[1]));
+ }.bind(this));
+ return newobj;
+ } else if (obj.hasOwnProperty('_ByteStream_')) {
+ if (obj._ByteStream_.hasOwnProperty('_ArraySize_')) {
+ newobj = this._decode(obj._ByteStream_);
+ } else {
+ if (this.base64)
+ newobj = new Blob(this.base64ToBytes(obj._ByteStream_), {
+ type: "octet/stream"
+ });
+ else
+ newobj = new Blob(obj._ByteStream_, {
+ type: "octet/stream"
+ });
+ }
+ return newobj;
+ } else if (obj.hasOwnProperty('_TableData_') &&
+ obj._TableData_.hasOwnProperty('_TableRecords_') &&
+ obj._TableData_._TableRecords_.length) {
+ newobj = {};
+ if (obj._TableData_._TableCols_.length == obj._TableData_._TableRecords_[0].length) {
+ obj._TableData_._TableCols_.forEach(function(e) {
+ newobj[e] = [];
+ });
+ obj._TableRecords_.forEach(function(e) {
+ for (let i = 0; i < e.length; i++)
+ newobj[obj._TableData_._TableCols_[i]].push(e[i]);
+ });
+ }
+ return newobj;
+ }
+ for (var k in obj) {
+ newobj[k] = this._decode(obj[k]);
+ }
+ }
+ return newobj;
+ }
+}
+
+try {
+ module.exports = jdata;
+} catch (e) {}
\ No newline at end of file
diff --git a/public/js/js_lzma.js b/public/js/js_lzma.js
new file mode 100644
index 0000000..17a9f49
--- /dev/null
+++ b/public/js/js_lzma.js
@@ -0,0 +1 @@
+var LZMA=LZMA||{};!function(e){"use strict";e.OutWindow=function(){this._windowSize=0},e.OutWindow.prototype.create=function(e){this._buffer&&this._windowSize===e||(this._buffer=new Uint8Array(e)),this._windowSize=e,this._pos=0,this._streamPos=0},e.OutWindow.prototype.flush=function(){var e=this._pos-this._streamPos;if(0!==e){if(this._stream.writeBytes)this._stream.writeBytes(this._buffer,e);else for(var t=0;t=this._windowSize&&(this._pos=0),this._streamPos=this._pos}},e.OutWindow.prototype.releaseStream=function(){this.flush(),this._stream=null},e.OutWindow.prototype.setStream=function(e){this.releaseStream(),this._stream=e},e.OutWindow.prototype.init=function(e){e||(this._streamPos=0,this._pos=0)},e.OutWindow.prototype.copyBlock=function(e,t){var i=this._pos-e-1;for(i<0&&(i+=this._windowSize);t--;)i>=this._windowSize&&(i=0),this._buffer[this._pos++]=this._buffer[i++],this._pos>=this._windowSize&&this.flush()},e.OutWindow.prototype.putByte=function(e){this._buffer[this._pos++]=e,this._pos>=this._windowSize&&this.flush()},e.OutWindow.prototype.getByte=function(e){var t=this._pos-e-1;return t<0&&(t+=this._windowSize),this._buffer[t]},e.RangeDecoder=function(){},e.RangeDecoder.prototype.setStream=function(e){this._stream=e},e.RangeDecoder.prototype.releaseStream=function(){this._stream=null},e.RangeDecoder.prototype.init=function(){var e=5;for(this._code=0,this._range=-1;e--;)this._code=this._code<<8|this._stream.readByte()},e.RangeDecoder.prototype.decodeDirectBits=function(e){for(var t,i=0,o=e;o--;)this._range>>>=1,t=this._code-this._range>>>31,this._code-=this._range&t-1,i=i<<1|1-t,(4278190080&this._range)==0&&(this._code=this._code<<8|this._stream.readByte(),this._range<<=8);return i},e.RangeDecoder.prototype.decodeBit=function(e,t){var i=e[t],o=(this._range>>>11)*i;return(2147483648^this._code)<(2147483648^o)?(this._range=o,e[t]+=2048-i>>>5,(4278190080&this._range)==0&&(this._code=this._code<<8|this._stream.readByte(),this._range<<=8),0):(this._range-=o,this._code-=o,e[t]-=i>>>5,(4278190080&this._range)==0&&(this._code=this._code<<8|this._stream.readByte(),this._range<<=8),1)},e.initBitModels=function(e,t){for(;t--;)e[t]=1024},e.BitTreeDecoder=function(e){this._models=[],this._numBitLevels=e},e.BitTreeDecoder.prototype.init=function(){e.initBitModels(this._models,1<>7&1,t<<=1,o=e.decodeBit(this._decoders,(1+i<<8)+r),r=r<<1|o,i!==o){for(;r<256;)r=r<<1|e.decodeBit(this._decoders,r);break}while(r<256);return 255&r},e.LiteralDecoder=function(){},e.LiteralDecoder.prototype.create=function(t,i){var o;if(!this._coders||this._numPrevBits!==i||this._numPosBits!==t)for(this._numPosBits=t,this._posMask=(1<>>8-this._numPrevBits)]},e.Decoder=function(){this._outWindow=new e.OutWindow,this._rangeDecoder=new e.RangeDecoder,this._isMatchDecoders=[],this._isRepDecoders=[],this._isRepG0Decoders=[],this._isRepG1Decoders=[],this._isRepG2Decoders=[],this._isRep0LongDecoders=[],this._posSlotDecoder=[],this._posDecoders=[],this._posAlignDecoder=new e.BitTreeDecoder(4),this._lenDecoder=new e.LenDecoder,this._repLenDecoder=new e.LenDecoder,this._literalDecoder=new e.LiteralDecoder,this._dictionarySize=-1,this._dictionarySizeCheck=-1,this._posSlotDecoder[0]=new e.BitTreeDecoder(6),this._posSlotDecoder[1]=new e.BitTreeDecoder(6),this._posSlotDecoder[2]=new e.BitTreeDecoder(6),this._posSlotDecoder[3]=new e.BitTreeDecoder(6)},e.Decoder.prototype.setDictionarySize=function(e){return!(e<0)&&(this._dictionarySize!==e&&(this._dictionarySize=e,this._dictionarySizeCheck=Math.max(this._dictionarySize,1),this._outWindow.create(Math.max(this._dictionarySizeCheck,4096))),!0)},e.Decoder.prototype.setLcLpPb=function(e,t,i){var o=1<8)&&!(t>4)&&!(i>4)&&(this._literalDecoder.create(t,e),this._lenDecoder.create(o),this._repLenDecoder.create(o),this._posStateMask=o-1,!0)},e.Decoder.prototype.setProperties=function(e){if(!this.setLcLpPb(e.lc,e.lp,e.pb))throw Error("Incorrect stream properties");if(!this.setDictionarySize(e.dictionarySize))throw Error("Invalid dictionary size")},e.Decoder.prototype.decodeHeader=function(e){var t,i,o,r,s,d;return!(e.size<13)&&(i=(t=e.readByte())%9,o=(t=~~(t/9))%5,r=~~(t/5),d=e.readByte(),d|=e.readByte()<<8,d|=e.readByte()<<16,d+=16777216*e.readByte(),s=e.readByte(),s|=e.readByte()<<8,s|=e.readByte()<<16,s+=16777216*e.readByte(),e.readByte(),e.readByte(),e.readByte(),e.readByte(),{lc:i,lp:o,pb:r,dictionarySize:d,uncompressedSize:s})},e.Decoder.prototype.init=function(){var t=4;for(this._outWindow.init(!1),e.initBitModels(this._isMatchDecoders,192),e.initBitModels(this._isRep0LongDecoders,192),e.initBitModels(this._isRepDecoders,12),e.initBitModels(this._isRepG0Decoders,12),e.initBitModels(this._isRepG1Decoders,12),e.initBitModels(this._isRepG2Decoders,12),e.initBitModels(this._posDecoders,114),this._literalDecoder.init();t--;)this._posSlotDecoder[t].init();this._lenDecoder.init(),this._repLenDecoder.init(),this._posAlignDecoder.init(),this._rangeDecoder.init()},e.Decoder.prototype.decodeBody=function(t,i,o){var r,s,d,n,c,h,a=0,p=0,u=0,D=0,$=0,f=0,B=0;for(this._rangeDecoder.setStream(t),this._outWindow.setStream(i),this.init();o<0||f=7?s.decodeWithMatchByte(this._rangeDecoder,this._outWindow.getByte(p)):s.decodeNormal(this._rangeDecoder),this._outWindow.putByte(B),a=a<4?0:a-(a<10?3:6);else{if(1===this._rangeDecoder.decodeBit(this._isRepDecoders,a))d=0,0===this._rangeDecoder.decodeBit(this._isRepG0Decoders,a)?0===this._rangeDecoder.decodeBit(this._isRep0LongDecoders,(a<<4)+r)&&(a=a<7?9:11,d=1):(0===this._rangeDecoder.decodeBit(this._isRepG1Decoders,a)?n=u:(0===this._rangeDecoder.decodeBit(this._isRepG2Decoders,a)?n=D:(n=$,$=D),D=u),u=p,p=n),0===d&&(d=2+this._repLenDecoder.decode(this._rangeDecoder,r),a=a<7?8:11);else if($=D,D=u,u=p,d=2+this._lenDecoder.decode(this._rangeDecoder,r),a=a<7?7:10,(c=this._posSlotDecoder[d<=5?d-2:3].decode(this._rangeDecoder))>=4){if(h=(c>>1)-1,p=(2|1&c)<=f||p>=this._dictionarySizeCheck)return!1;this._outWindow.copyBlock(p,d),f+=d,B=this._outWindow.getByte(0)}return this._outWindow.flush(),this._outWindow.releaseStream(),this._rangeDecoder.releaseStream(),!0},e.Decoder.prototype.setDecoderProperties=function(e){var t,i,o,r,s;return!(e.size<5)&&(i=(t=e.readByte())%9,o=(t=~~(t/9))%5,r=~~(t/5),!!this.setLcLpPb(i,o,r)&&(s=e.readByte(),s|=e.readByte()<<8,s|=e.readByte()<<16,s+=16777216*e.readByte(),this.setDictionarySize(s)))},e.decompress=function(t,i,o,r){var s=new e.Decoder;if(!s.setDecoderProperties(t))throw Error("Incorrect lzma stream properties");if(!s.decodeBody(i,o,r))throw Error("Error in lzma data stream");return o},e.decompressFile=function(t,i){t instanceof ArrayBuffer&&(t=new e.iStream(t)),!i&&e.oStream&&(i=new e.oStream);var o=new e.Decoder,r=o.decodeHeader(t),s=r.uncompressedSize;if(o.setProperties(r),!o.decodeBody(t,i,s))throw Error("Error in lzma data stream");return i},e.decode=e.decompressFile}(LZMA);
\ No newline at end of file
diff --git a/public/js/js_lzma_shim.js b/public/js/js_lzma_shim.js
new file mode 100644
index 0000000..1c45fe1
--- /dev/null
+++ b/public/js/js_lzma_shim.js
@@ -0,0 +1 @@
+var LZMA=LZMA||{};!function(t){t.iStream=function(t){this.array=new Uint8Array(t),this.size=t.byteLength,this.offset=0},t.iStream.prototype.readByte=function(){return this.array[this.offset++]},t.oStream=function(r){this.size=0,this.buffers=[],r=r||[];for(var e=0,o=r.length;e(()=>{"use strict";var e={767:(e,n,t)=>{t.d(n,{Z:()=>c});var r=t(81),o=t.n(r),i=t(645),a=t.n(i)()(o());a.push([e.id,'.json-container{font-family:"Open Sans";font-size:16px;background-color:#fff;color:gray;box-sizing:border-box}.json-container .line{margin:4px 0;display:flex;justify-content:flex-start}.json-container .caret-icon{width:18px;text-align:center;cursor:pointer}.json-container .empty-icon{width:18px}.json-container .json-type{margin-right:4px;margin-left:4px}.json-container .json-key{color:#444;margin-right:4px;margin-left:4px}.json-container .json-index{margin-right:4px;margin-left:4px}.json-container .json-value{margin-left:8px}.json-container .json-number{color:#f9ae58}.json-container .json-boolean{color:#ec5f66}.json-container .json-string{color:#86b25c}.json-container .json-size{margin-right:4px;margin-left:4px}.json-container .hidden{display:none}.json-container .fas{display:inline-block;border-style:solid;width:0;height:0}.json-container .fa-caret-down{border-width:6px 5px 0 5px;border-color:gray rgba(0,0,0,0)}.json-container .fa-caret-right{border-width:5px 0 5px 6px;border-color:rgba(0,0,0,0) rgba(0,0,0,0) rgba(0,0,0,0) gray}',""]);const c=a},645:e=>{e.exports=function(e){var n=[];return n.toString=function(){return this.map((function(n){var t="",r=void 0!==n[5];return n[4]&&(t+="@supports (".concat(n[4],") {")),n[2]&&(t+="@media ".concat(n[2]," {")),r&&(t+="@layer".concat(n[5].length>0?" ".concat(n[5]):""," {")),t+=e(n),r&&(t+="}"),n[2]&&(t+="}"),n[4]&&(t+="}"),t})).join("")},n.i=function(e,t,r,o,i){"string"==typeof e&&(e=[[null,e,void 0]]);var a={};if(r)for(var c=0;c0?" ".concat(d[5]):""," {").concat(d[1],"}")),d[5]=i),t&&(d[2]?(d[1]="@media ".concat(d[2]," {").concat(d[1],"}"),d[2]=t):d[2]=t),o&&(d[4]?(d[1]="@supports (".concat(d[4],") {").concat(d[1],"}"),d[4]=o):d[4]="".concat(o)),n.push(d))}},n}},81:e=>{e.exports=function(e){return e[1]}},379:e=>{var n=[];function t(e){for(var t=-1,r=0;r{var n={};e.exports=function(e,t){var r=function(e){if(void 0===n[e]){var t=document.querySelector(e);if(window.HTMLIFrameElement&&t instanceof window.HTMLIFrameElement)try{t=t.contentDocument.head}catch(e){t=null}n[e]=t}return n[e]}(e);if(!r)throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");r.appendChild(t)}},216:e=>{e.exports=function(e){var n=document.createElement("style");return e.setAttributes(n,e.attributes),e.insert(n,e.options),n}},565:(e,n,t)=>{e.exports=function(e){var n=t.nc;n&&e.setAttribute("nonce",n)}},795:e=>{e.exports=function(e){if("undefined"==typeof document)return{update:function(){},remove:function(){}};var n=e.insertStyleElement(e);return{update:function(t){!function(e,n,t){var r="";t.supports&&(r+="@supports (".concat(t.supports,") {")),t.media&&(r+="@media ".concat(t.media," {"));var o=void 0!==t.layer;o&&(r+="@layer".concat(t.layer.length>0?" ".concat(t.layer):""," {")),r+=t.css,o&&(r+="}"),t.media&&(r+="}"),t.supports&&(r+="}");var i=t.sourceMap;i&&"undefined"!=typeof btoa&&(r+="\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(i))))," */")),n.styleTagTransform(r,e,n.options)}(n,e,t)},remove:function(){!function(e){if(null===e.parentNode)return!1;e.parentNode.removeChild(e)}(n)}}}},589:e=>{e.exports=function(e,n){if(n.styleSheet)n.styleSheet.cssText=e;else{for(;n.firstChild;)n.removeChild(n.firstChild);n.appendChild(document.createTextNode(e))}}}},n={};function t(r){var o=n[r];if(void 0!==o)return o.exports;var i=n[r]={id:r,exports:{}};return e[r](i,i.exports,t),i.exports}t.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return t.d(n,{a:n}),n},t.d=(e,n)=>{for(var r in n)t.o(n,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},t.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.nc=void 0;var r={};return(()=>{t.r(r),t.d(r,{collapse:()=>A,create:()=>S,default:()=>H,destroy:()=>L,expand:()=>D,render:()=>w,renderJSON:()=>k,toggleNode:()=>N,traverse:()=>C});var e=t(379),n=t.n(e),o=t(795),i=t.n(o),a=t(569),c=t.n(a),s=t(565),l=t.n(s),d=t(216),u=t.n(d),p=t(589),f=t.n(p),v=t(767),y={};function h(e){return Array.isArray(e)?"array":null===e?"null":typeof e}function g(e){return document.createElement(e)}y.styleTagTransform=f(),y.setAttributes=l(),y.insert=c().bind(null,"head"),y.domAPI=i(),y.insertStyleElement=u(),n()(v.Z,y),v.Z&&v.Z.locals&&v.Z.locals;const m={HIDDEN:"hidden",CARET_ICON:"caret-icon",CARET_RIGHT:"fa-caret-right",CARET_DOWN:"fa-caret-down",ICON:"fas"};function x(e){e.children.forEach((e=>{e.el.classList.add(m.HIDDEN),e.isExpanded&&x(e)}))}function b(e){e.children.forEach((e=>{e.el.classList.remove(m.HIDDEN),e.isExpanded&&b(e)}))}function j(e){if(e.children.length>0){const n=e.el.querySelector("."+m.ICON);n&&n.classList.replace(m.CARET_RIGHT,m.CARET_DOWN)}}function E(e){if(e.children.length>0){const n=e.el.querySelector("."+m.ICON);n&&n.classList.replace(m.CARET_DOWN,m.CARET_RIGHT)}}function N(e){e.isExpanded?(e.isExpanded=!1,E(e),x(e)):(e.isExpanded=!0,j(e),b(e))}function C(e,n){if("array"===e.type&&e.children.length>0&&e.children.every((function(e){return"number"===e.type||"n/a"===e.value}))){e.value="[";for(let n=0;n0?", ":"")+e.children[n].value;e.value+="]",e.children=[],e.type="string"}n(e),e.children.length>0&&e.children.forEach((e=>{C(e,n)}))}function T(e={}){let n=e.hasOwnProperty("value")?e.value:null;return(e=>"object"===h(e)&&0===Object.keys(e).length)(n)&&(n="{}"),{key:e.key||null,parent:e.parent||null,value:n,isExpanded:e.isExpanded||!1,type:e.type||null,children:e.children||[],el:e.el||null,depth:e.depth||0,dispose:null}}function I(e,n){if("object"==typeof e)for(let t in e){const r=T({value:e[t],key:t,depth:n.depth+1,type:h(e[t]),parent:n});n.children.push(r),I(e[t],r)}}function O(e){return"string"==typeof e?JSON.parse(e):e}function S(e){const n=O(e),t=T({value:n,key:h(n),type:h(n)});return I(n,t),t}function k(e,n){const t=S(O(e));return w(t,n),t}function w(e,n){const t=function(){const e=g("div");return e.className="json-container",e}();C(e,(function(e){e.el=function(e){let n=g("div");const t=e=>{const n=e.children.length;return"array"===e.type?`[${n}]`:"object"===e.type?`{${n}}`:null};if(e.children.length>0){n.innerHTML=function(e={}){const{key:n,size:t}=e;return`\n \n `}({key:e.key,size:t(e)});const r=n.querySelector("."+m.CARET_ICON);e.dispose=function(e,n,t){return e.addEventListener(n,t),()=>e.removeEventListener(n,t)}(r,"click",(()=>N(e)))}else n.innerHTML=function(e={}){const{key:n,value:t,type:r}=e;return`\n \n `}({key:e.key,value:e.value,type:"{}"===e.value?"object":typeof e.value});const r=n.children[0];return null!==e.parent&&r.classList.add(m.HIDDEN),r.style="margin-left: "+18*e.depth+"px;",r}(e),t.appendChild(e.el)})),n.appendChild(t)}function D(e){C(e,(function(e){e.el.classList.remove(m.HIDDEN),e.isExpanded=!0,j(e)}))}function A(e){C(e,(function(n){n.isExpanded=!1,n.depth>e.depth&&n.el.classList.add(m.HIDDEN),E(n)}))}function L(e){var n;C(e,(e=>{e.dispose&&e.dispose()})),(n=e.el.parentNode).parentNode.removeChild(n)}const H={toggleNode:N,render:w,create:S,renderJSON:k,expand:D,collapse:A,traverse:C,destroy:L}})(),r})()));
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index fb09efa..92a3d73 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,16 +1,24 @@
+import { GlobalStyles } from "@mui/material";
import { ThemeProvider } from "@mui/material/styles";
import Routes from "components/Routes";
import theme from "design/theme";
import { BrowserRouter } from "react-router-dom";
const App = () => {
- return (
-
-
-
-
-
- );
+ return (
+
+
+
+
+
+
+ );
};
export default App;
diff --git a/src/components/DatasetDetailPage/LoadDatasetTabs.tsx b/src/components/DatasetDetailPage/LoadDatasetTabs.tsx
new file mode 100644
index 0000000..17c00e3
--- /dev/null
+++ b/src/components/DatasetDetailPage/LoadDatasetTabs.tsx
@@ -0,0 +1,416 @@
+import CloudDownloadIcon from "@mui/icons-material/CloudDownload";
+import ContentCopyIcon from "@mui/icons-material/ContentCopy";
+import {
+ Tabs,
+ Tab,
+ Box,
+ Typography,
+ IconButton,
+ Tooltip,
+ Link,
+ Button,
+} from "@mui/material";
+import { Colors } from "design/theme";
+import React from "react";
+import { useState } from "react";
+import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
+import bash from "react-syntax-highlighter/dist/esm/languages/hljs/bash";
+import cpp from "react-syntax-highlighter/dist/esm/languages/hljs/cpp";
+import javascript from "react-syntax-highlighter/dist/esm/languages/hljs/javascript";
+import matlab from "react-syntax-highlighter/dist/esm/languages/hljs/matlab";
+import python from "react-syntax-highlighter/dist/esm/languages/hljs/python";
+import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
+
+// import { Color } from "three";
+
+// Register language theme
+SyntaxHighlighter.registerLanguage("python", python);
+SyntaxHighlighter.registerLanguage("bash", bash);
+SyntaxHighlighter.registerLanguage("cpp", cpp);
+SyntaxHighlighter.registerLanguage("matlab", matlab);
+SyntaxHighlighter.registerLanguage("javascript", javascript);
+interface LoadDatasetTabsProps {
+ pagename: string;
+ docname: string;
+ dbname: string;
+ serverUrl: string;
+ datasetDocument?: any;
+ onekey: string;
+ handleDownloadDataset: () => void;
+}
+
+const flashcardStyles = {
+ container: {
+ display: "flex",
+ flexWrap: "wrap",
+ justifyContent: "space-between",
+ marginTop: "20px",
+ gap: "0px",
+ },
+ flashcard: {
+ background: "white",
+ padding: "15px",
+ marginBottom: "0px",
+ borderRadius: "8px",
+ borderLeft: `5px solid ${Colors.green}`,
+ width: "100%",
+ boxSizing: "border-box" as const,
+ },
+ flashcardTitle: {
+ marginTop: 0,
+ marginBottom: 8,
+ fontWeight: "bold",
+ color: Colors.darkPurple,
+ },
+ codeBlock: {
+ display: "block",
+ background: "#e0e0e0",
+ color: Colors.black,
+ padding: "10px",
+ borderRadius: "5px",
+ whiteSpace: "pre-wrap",
+ },
+};
+
+const LoadDatasetTabs: React.FC = ({
+ pagename,
+ docname,
+ dbname,
+ serverUrl,
+ datasetDocument,
+ onekey,
+ handleDownloadDataset,
+}) => {
+ const [tabIndex, setTabIndex] = useState(0);
+ const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
+ setTabIndex(newValue);
+ };
+ // console.log("datasetDocument", datasetDocument);
+ const datasetDesc = datasetDocument?.["dataset_description.json"];
+ // console.log("datasetDesc", datasetDesc);
+ const datasetName = datasetDesc?.Name?.includes(" - ")
+ ? datasetDesc.Name.split(" - ")[1]
+ : datasetDesc?.Name || datasetDocument?._id || docname;
+ console.log("datasetName", datasetName);
+ console.log("dbname", dbname);
+ console.log("pagename", pagename);
+ console.log("onekey", onekey);
+ // const datasetUrl = datasetName
+ // ? `${serverUrl}${dbname}/${encodeURIComponent(datasetName)}/`
+ // : `${serverUrl}${dbname}/`;
+ const datasetUrl = datasetName
+ ? `${serverUrl}${dbname}/${pagename}`
+ : `${serverUrl}${dbname}/`;
+
+ const TabPanel = ({
+ children,
+ value,
+ index,
+ }: {
+ children?: React.ReactNode;
+ value: number;
+ index: number;
+ }) => {
+ return (
+
+ {value === index && {children}}
+
+ );
+ };
+
+ const CopyableCodeBlock = ({
+ code,
+ language = "python",
+ }: {
+ code: string;
+ language?: string;
+ }) => {
+ // const handleCopy = () => {
+ // navigator.clipboard.writeText(code);
+ // };
+ const [copied, setCopied] = useState(false);
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(code);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 3000); // reset after 3s
+ } catch (err) {
+ console.error("Failed to copy:", err);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {code}
+
+
+
+ );
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {/* FLASHCARD 1: Python REST API */}
+
+
+
+ Load by URL with REST-API in Python
+
+ Install:
+
+ Load from URL:
+
+
+
+
+ {/* FLASHCARD 2: MATLAB REST API */}
+
+
+
+ Load by URL with REST-API in MATLAB
+
+ Install:
+
+ Download and addpath to{" "}
+
+ JSONLab
+
+
+ Load from URL:
+
+
+
+
+ {/* FLASHCARD 3: Python Local */}
+
+
+
+ Use in Python
+
+
+ Download dataset
+ }
+ onClick={handleDownloadDataset}
+ sx={{
+ backgroundColor: Colors.purple,
+ color: Colors.lightGray,
+ mb: 2,
+ "&:hover": {
+ backgroundColor: Colors.secondaryPurple,
+ },
+ }}
+ >
+ Download Metadata
+
+ Load:
+
+ Read value:
+
+
+
+
+ {/* FLASHCARD 4: MATLAB/Octave */}
+
+
+
+ Use in MATLAB/Octave
+
+ Download dataset
+ }
+ onClick={handleDownloadDataset}
+ sx={{
+ backgroundColor: Colors.purple,
+ color: Colors.lightGray,
+ mb: 2,
+ "&:hover": {
+ backgroundColor: Colors.secondaryPurple,
+ },
+ }}
+ >
+ Download Metadata
+
+ Load:
+
+ Read value:
+
+
+
+
+ {/* FLASHCARD 5: C++ */}
+
+
+
+ Use in C++
+
+ Install:
+ {/* */}
+
+ Download{" "}
+
+ JSON for Modern C++ json.hpp
+
+
+ Load:
+
+ Read value:
+
+
+
+
+ {/* FLASHCARD 6: Node.js */}
+
+
+
+ Use in JS/Node.js
+
+ Install:
+
+ Load:
+
+ Read value:
+
+
+
+ >
+ );
+};
+
+export default LoadDatasetTabs;
diff --git a/src/components/DatasetFlashcards.tsx b/src/components/DatasetFlashcards.tsx
new file mode 100644
index 0000000..0fef88d
--- /dev/null
+++ b/src/components/DatasetFlashcards.tsx
@@ -0,0 +1,186 @@
+import React from "react";
+import { Box, Typography } from "@mui/material";
+
+interface DatasetFlashcardsProps {
+ pagename: string;
+ docname: string;
+ dbname: string;
+ serverUrl: string;
+ datasetDocument?: any;
+ onekey: string;
+}
+
+const flashcardStyles = {
+ container: {
+ display: "flex",
+ flexWrap: "wrap",
+ justifyContent: "space-between",
+ marginTop: "20px",
+ gap: "0px",
+ },
+ flashcard: {
+ background: "white",
+ padding: "15px",
+ marginBottom: "0px",
+ borderRadius: "8px",
+ borderLeft: "5px solid #6200ea",
+ width: "calc(50% - 8px)", // Two per row, tight spacing
+ boxSizing: "border-box" as const,
+ },
+ flashcardTitle: {
+ marginTop: 0,
+ fontWeight: "bold",
+ },
+ codeBlock: {
+ display: "block",
+ background: "#e0e0e0",
+ padding: "10px",
+ borderRadius: "5px",
+ fontFamily: "monospace",
+ whiteSpace: "pre-wrap",
+ },
+};
+
+const DatasetFlashcards: React.FC = ({
+ pagename,
+ docname,
+ dbname,
+ serverUrl,
+ datasetDocument,
+ onekey,
+}) => {
+ const datasetDesc = datasetDocument?.["dataset_description.json"];
+ const datasetName = datasetDesc?.Name?.includes(" - ")
+ ? datasetDesc.Name.split(" - ")[1]
+ : datasetDesc?.Name || datasetDocument?._id || docname;
+
+ const datasetUrl = datasetName
+ ? `${serverUrl}${dbname}/${encodeURIComponent(datasetName)}/`
+ : `${serverUrl}${dbname}/`;
+
+ return (
+
+ {/* FLASHCARD 1: Python REST API */}
+
+
+ Load by URL with REST-API in Python
+
+ Install:
+ pip install jdata bjdata numpy
+ Load from URL:
+
+ {`import jdata as jd
+data = jd.loadurl('${datasetUrl}')
+
+# List all externally linked files
+links = jd.jsonpath(data, '$.._DataLink_')
+
+# Download & cache anatomical nii.gz data for sub-01/sub-02
+jd.jdlink(links, {'regex': 'anat/sub-0[12]_.*\\.nii'})`}
+
+
+
+ {/* FLASHCARD 2: MATLAB REST API */}
+
+
+ Load by URL with REST-API in MATLAB
+
+ Install:
+ Download and addpath to JSONLab
+ Load from URL:
+
+ {`data = loadjson('${datasetUrl}');
+
+% or without JSONLab (webread cannot decode JData annotations)
+data = webread('${datasetUrl}');
+
+% List all externally linked files
+links = jsonpath(data, '$.._DataLink_');
+
+% Download & cache anatomical nii.gz data for sub-01/sub-02
+niidata = jdlink(links, 'regex', 'anat/sub-0[12]_.*\\.nii');`}
+
+
+
+ {/* FLASHCARD 3: MATLAB/Octave */}
+
+
+ Use in MATLAB/Octave
+
+ Load:
+
+ {`data = loadjd('${docname}.json');`}
+
+ Read value:
+
+ {`data.(encodevarname('${onekey}'))`}
+
+
+
+ {/* FLASHCARD 4: Python Local */}
+
+
+ Use in Python
+
+ Load:
+
+ {`import jdata as jd
+data = jd.load('${docname}.json')`}
+
+ Read value:
+
+ {`data["${onekey}"]`}
+
+
+
+ {/* FLASHCARD 5: C++ */}
+
+
+ Use in C++
+
+ Install:
+
+ Download JSON for Modern C++ json.hpp
+
+ Load:
+
+ {`#include "json.hpp"
+using json=nlohmann::ordered_json;
+
+std::ifstream datafile("${docname}.json");
+json data(datafile);`}
+
+ Read value:
+
+ {`std::cout << data["${onekey}"];`}
+
+
+
+ {/* FLASHCARD 6: Node.js */}
+
+
+ Use in JS/Node.js
+
+ Install:
+ npm install jda numjs pako atob
+ Load:
+
+ {`const fs = require("fs");
+const jd = require("jda");
+global.atob = require("atob");
+
+const fn = "${docname}.json";
+var jstr = fs.readFileSync(fn).toString().replace(/\\n/g, "");
+var data = new jd(JSON.parse(jstr));
+data = data.decode();`}
+
+ Read value:
+
+ {`console.log(data.data["${onekey}"]);`}
+
+
+
+ );
+};
+
+export default DatasetFlashcards;
diff --git a/src/components/DatasetPageCard.tsx b/src/components/DatasetPageCard.tsx
new file mode 100644
index 0000000..c3d9d17
--- /dev/null
+++ b/src/components/DatasetPageCard.tsx
@@ -0,0 +1,163 @@
+import {
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Chip,
+ Link,
+ Stack,
+ Typography,
+ Grid,
+} from "@mui/material";
+import { Colors } from "design/theme";
+import React from "react";
+import { useNavigate } from "react-router-dom";
+import { Row } from "redux/neurojson/types/neurojson.interface";
+import RoutesEnum from "types/routes.enum";
+
+interface DatasetPageCardProps {
+ doc: Row;
+ index: number;
+ dbName: string;
+ pageSize: number;
+ page: number;
+}
+
+const DatasetPageCard: React.FC = ({
+ doc,
+ index,
+ dbName,
+ page,
+ pageSize,
+}) => {
+ const navigate = useNavigate();
+ const datasetIndex = (page - 1) * pageSize + index + 1;
+ return (
+
+
+
+ #{datasetIndex}
+
+
+
+
+
+ ID: {doc.id}
+
+
+
+
+ {doc.value.subj && (
+
+ )}
+ {doc.value.modality &&
+ doc.value.modality.map((mod: string) => (
+
+ ))}
+
+
+
+ Summary:{" "}
+ {doc.value.readme || "No description available"}
+
+
+
+ Authors:{" "}
+ {Array.isArray(doc.value.info?.Authors)
+ ? doc.value.info.Authors.join(", ")
+ : doc.value.info?.Authors || "Unknown"}
+
+
+
+
+ Size:{" "}
+ {doc.value.length
+ ? `${(doc.value.length / 1024 / 1024).toFixed(2)} MB`
+ : "Unknown"}
+
+
+ {doc.value.info?.DatasetDOI && (
+
+
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default DatasetPageCard;
diff --git a/src/components/HomePageComponents/Section1.tsx b/src/components/HomePageComponents/Section1.tsx
new file mode 100644
index 0000000..57a4a01
--- /dev/null
+++ b/src/components/HomePageComponents/Section1.tsx
@@ -0,0 +1,222 @@
+import ArrowCircleDownIcon from "@mui/icons-material/ArrowCircleDown";
+import SearchIcon from "@mui/icons-material/Search";
+import {
+ Typography,
+ Box,
+ Button,
+ TextField,
+ Grid,
+ IconButton,
+} from "@mui/material";
+import StatisticsBanner from "components/StatisticsBanner";
+import { Colors } from "design/theme";
+import pako from "pako";
+import React, { useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+interface Section1Props {
+ scrollToNext: () => void;
+}
+
+const Section1: React.FC = ({ scrollToNext }) => {
+ const [keyword, setKeyword] = useState("");
+ const navigate = useNavigate();
+
+ const handleSearch = () => {
+ if (!keyword.trim()) return;
+
+ const queryData = { keyword };
+ const deflated = pako.deflate(JSON.stringify(queryData));
+ const encoded = btoa(String.fromCharCode(...deflated));
+
+ navigate(`/search#query=${encoded}`);
+ };
+
+ return (
+
+
+ {/* first row: logo and text */}
+
+
+
+
+ Search Findable, Accessible, Interoperable, and Reusable datasets
+
+
+
+
+ {/* search row */}
+
+
+ setKeyword(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ handleSearch();
+ }
+ }}
+ sx={{
+ backgroundColor: Colors.white,
+ borderRadius: "8px",
+ "& .MuiInputLabel-root": {
+ color: "gray", // label text color
+ },
+ "& .MuiInputBase-input": {
+ color: Colors.purple, // text inside the input
+ },
+ "& label.Mui-focused": {
+ color: Colors.darkGreen, // label color when focused
+ fontWeight: "bold",
+ transform: "translate(12px, -22px) scale(0.75)", // translate(x, y) move label further up
+ padding: "5px",
+ borderRadius: "5px",
+ },
+ "& .MuiOutlinedInput-root": {
+ "& fieldset": {
+ borderColor: "none", // default border color
+ },
+ "&:hover fieldset": {
+ // borderColor: Colors.pink, // hover border color
+ },
+ "&.Mui-focused fieldset": {
+ borderColor: Colors.darkGreen, // border when focused
+ },
+ },
+ }}
+ />
+
+
+
+
+ {/* statistics banner row*/}
+
+
+
+
+
+ {/* Scroll Arrow */}
+
+
+ {/* */}
+ {/* */}
+
+
+
+
+ );
+};
+
+export default Section1;
diff --git a/src/components/HomePageComponents/Section2.tsx b/src/components/HomePageComponents/Section2.tsx
new file mode 100644
index 0000000..f9329ca
--- /dev/null
+++ b/src/components/HomePageComponents/Section2.tsx
@@ -0,0 +1,190 @@
+import ArrowCircleDownIcon from "@mui/icons-material/ArrowCircleDown";
+import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
+import { Typography, Box, Button, IconButton } from "@mui/material";
+import { Colors } from "design/theme";
+import ForceGraphModal from "modules/universe/ForceGraphModal";
+import { NodeObject } from "modules/universe/NeuroJsonGraph";
+import React from "react";
+import { useNavigate } from "react-router-dom";
+import RoutesEnum from "types/routes.enum";
+
+interface Section2Props {
+ registry: any[] | null;
+ filteredRegistry: any[];
+ filterKeyword: string;
+ selectedModalities: string[];
+ setFilterKeyword: (keyword: string) => void;
+ setSelectedModalities: (modalities: string[]) => void;
+ onNodeClick: (node: NodeObject) => void;
+ scrollToNext: () => void;
+}
+
+const Section2: React.FC = ({
+ registry,
+ filteredRegistry,
+ filterKeyword,
+ selectedModalities,
+ setFilterKeyword,
+ setSelectedModalities,
+ onNodeClick,
+ scrollToNext,
+}) => {
+ const navigate = useNavigate();
+
+ return (
+
+ {/* title, text and buttons */}
+
+ {/* title and text */}
+
+
+ Discover and Explore
+
+
+ Dive into our interactive 3D graph to explore neuroimaging
+ databases. Visualize meaningful connections, filter by modality, and
+ access rich metadata instantly.
+
+
+
+ {/* buttons */}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Scroll Arrow */}
+
+
+
+
+
+
+ );
+};
+
+export default Section2;
diff --git a/src/components/HomePageComponents/Section3.tsx b/src/components/HomePageComponents/Section3.tsx
new file mode 100644
index 0000000..5f7fa06
--- /dev/null
+++ b/src/components/HomePageComponents/Section3.tsx
@@ -0,0 +1,152 @@
+import ArrowCircleDownIcon from "@mui/icons-material/ArrowCircleDown";
+import CloseIcon from "@mui/icons-material/Close";
+import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
+import { Typography, Box, IconButton, Dialog } from "@mui/material";
+import { Colors } from "design/theme";
+import React from "react";
+import { useState } from "react";
+
+interface Section3Props {
+ scrollToNext: () => void;
+}
+
+const Section3: React.FC = ({ scrollToNext }) => {
+ const [open, setOpen] = useState(false);
+
+ const handleOpen = () => setOpen(true);
+ const handleClose = () => setOpen(false);
+
+ return (
+
+ {/* title and text */}
+
+
+ Visualize data via our preview tools
+
+
+ Preview and explore neuroimaging data with interactive visualization
+ tools directly in NeuroJSON.io. Adjust display modes, customize
+ colormaps, slice across dimensions, and interactively rotate and zoom
+ models.
+
+
+
+
+
+
+
+ {/* video dialog */}
+
+
+ {/* Scroll Arrow to Section4 */}
+
+
+
+
+
+
+ );
+};
+
+export default Section3;
diff --git a/src/components/HomePageComponents/Section4.tsx b/src/components/HomePageComponents/Section4.tsx
new file mode 100644
index 0000000..207a150
--- /dev/null
+++ b/src/components/HomePageComponents/Section4.tsx
@@ -0,0 +1,125 @@
+import { Box, Typography, Slide } from "@mui/material";
+import WorkflowDetailCard from "components/WorkflowDetailCard";
+import { Colors } from "design/theme";
+import React, { useState } from "react";
+
+type CardType = "data-portals" | "json-conversion" | "database" | "rest-api";
+
+const cards: { type: CardType; src: string }[] = [
+ { type: "data-portals", src: "/img/section4/workflow1.png" },
+ { type: "json-conversion", src: "/img/section4/workflow2.png" },
+ { type: "database", src: "/img/section4/workflow3.png" },
+ { type: "rest-api", src: "/img/section4/workflow4.png" },
+];
+
+const Section4: React.FC = () => {
+ const [selectedCard, setSelectedCard] = useState(null);
+
+ return (
+
+ {/* title and text container */}
+
+
+ Platform Workflow
+
+
+
+ click each card to learn more
+
+
+
+ {selectedCard && (
+
+
+
+
+
+ )}
+
+
+ {/* img container */}
+
+ {cards.map((card, index, arr) => (
+ setSelectedCard(card.type)}
+ sx={{
+ maxWidth: "700px",
+ width: "100%",
+ cursor: "pointer",
+ position: "relative",
+ zIndex: arr.length - index,
+ marginTop: index === 0 ? 0 : "-20px", // adjust overlap
+ transition: "transform 0.2s",
+ "&:hover": {
+ transform: "scale(1.02)",
+ zIndex: 5,
+ },
+ }}
+ >
+
+
+ ))}
+
+
+ );
+};
+
+export default Section4;
diff --git a/src/components/NavBar/DefaultNavBar.tsx b/src/components/NavBar/DefaultNavBar.tsx
new file mode 100644
index 0000000..7ef7ee9
--- /dev/null
+++ b/src/components/NavBar/DefaultNavBar.tsx
@@ -0,0 +1,27 @@
+import NavItems from "./NavItems";
+import { AppBar } from "@mui/material";
+import React from "react";
+
+const DefaultNavBar: React.FC = () => {
+ return (
+
+
+
+ );
+};
+
+export default DefaultNavBar;
diff --git a/src/components/NavBar/LandingNavBar.tsx b/src/components/NavBar/LandingNavBar.tsx
new file mode 100644
index 0000000..813cd03
--- /dev/null
+++ b/src/components/NavBar/LandingNavBar.tsx
@@ -0,0 +1,35 @@
+import NavItems from "./NavItems";
+import { AppBar } from "@mui/material";
+import React from "react";
+
+interface LandingNavBarProps {
+ scrolled: boolean;
+}
+
+const LandingNavBar: React.FC = ({ scrolled }) => {
+ return (
+
+
+
+ );
+};
+
+export default LandingNavBar;
diff --git a/src/components/NavBar/NavItems.tsx b/src/components/NavBar/NavItems.tsx
new file mode 100644
index 0000000..2a2f9be
--- /dev/null
+++ b/src/components/NavBar/NavItems.tsx
@@ -0,0 +1,153 @@
+import { Toolbar, Grid, Button, Typography, Box } from "@mui/material";
+import { Colors } from "design/theme";
+import React from "react";
+import { useNavigate, Link } from "react-router-dom";
+import RoutesEnum from "types/routes.enum";
+
+const NavItems: React.FC = () => {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+ navigate("/")}
+ height="auto"
+ sx={{
+ height: "80px",
+ width: "auto",
+ display: { xs: "block", sm: "block", md: "none" },
+ }}
+ >
+
+
+
+
+ {/* Navigation links*/}
+
+
+ {[
+ // { text: "ABOUT", url: "https://neurojson.org/Doc/Start" },
+ { text: "About", url: RoutesEnum.ABOUT },
+ { text: "Wiki", url: "https://neurojson.org/Wiki" },
+ { text: "Search", url: RoutesEnum.SEARCH },
+ { text: "Databases", url: RoutesEnum.DATABASES },
+ { text: "V1", url: "https://neurojson.io/v1" },
+ ].map(({ text, url }) => (
+
+ {url?.startsWith("https") ? (
+
+
+ {text}
+
+
+ ) : (
+
+
+ {text}
+
+
+ )}
+
+ ))}
+
+
+
+
+ );
+};
+
+export default NavItems;
diff --git a/src/components/NodeInfoPanel.tsx b/src/components/NodeInfoPanel.tsx
new file mode 100644
index 0000000..60cfda1
--- /dev/null
+++ b/src/components/NodeInfoPanel.tsx
@@ -0,0 +1,344 @@
+import CloseIcon from "@mui/icons-material/Close";
+import {
+ Box,
+ Typography,
+ IconButton,
+ Drawer,
+ Grid,
+ Card,
+ CardContent,
+ CardActions,
+ Button,
+} from "@mui/material";
+import { Colors } from "design/theme";
+import { useAppDispatch } from "hooks/useAppDispatch";
+import { useAppSelector } from "hooks/useAppSelector";
+import { NodeObject } from "modules/universe/NeuroJsonGraph";
+import React, { useEffect, useRef } from "react";
+import { useNavigate } from "react-router-dom";
+import { fetchDbInfo } from "redux/neurojson/neurojson.action";
+import { RootState } from "redux/store";
+import RoutesEnum from "types/routes.enum";
+
+interface NodeInfoPanelProps {
+ open: boolean;
+ onClose: () => void;
+ nodeData: NodeObject | null;
+}
+
+// covert the database size format
+const formatSize = (bytes?: number): string => {
+ if (bytes === undefined) return "N/A";
+ if (bytes >= 1_073_741_824) {
+ return `${Math.floor(bytes / 1_073_741_824)} Gb`;
+ } else if (bytes >= 1_048_576) {
+ return `${Math.floor(bytes / 1_048_576)} Mb`;
+ } else if (bytes >= 1024) {
+ return `${Math.floor(bytes / 1024)} Kb`;
+ } else {
+ return `${bytes} Bytes`;
+ }
+};
+
+// convert the date format
+const dateCoverter = (date?: string): string => {
+ if (date === undefined) return "N/A";
+ const newDate = new Date(Number(date) * 1000);
+ const result = new Intl.DateTimeFormat("en-US", {
+ year: "numeric",
+ month: "numeric",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ second: "numeric",
+ }).format(newDate);
+ return result;
+};
+
+const NodeInfoPanel: React.FC = ({
+ open,
+ onClose,
+ nodeData,
+}) => {
+ const navigate = useNavigate();
+ const dispatch = useAppDispatch();
+ const dbInfo = useAppSelector((state: RootState) => state.neurojson.dbInfo);
+
+ useEffect(() => {
+ if (nodeData?.id) {
+ dispatch(fetchDbInfo(nodeData.id.toLowerCase()));
+ }
+ }, [nodeData, dispatch]);
+
+ return (
+ theme.zIndex.modal + 10,
+ "& .MuiDrawer-paper": {
+ width: { xs: "100%", sm: "40%", md: "30%" },
+ padding: "1rem",
+ boxShadow: `0px 0px 15px ${Colors.lightGray}`,
+ backgroundColor: "rgba(97, 109, 243, 0.1)",
+ backdropFilter: "blur(15px)",
+ },
+ }}
+ >
+
+ {nodeData ? (
+ <>
+ {/* Close Button */}
+
+
+ {nodeData.name}
+
+
+
+
+
+ {/* Node Metadata */}
+
+
+
+ Website
+
+
+ {nodeData.url}
+
+
+
+
+
+ Number of Datasets
+
+
+ {nodeData.datasets}
+
+
+
+
+
+ Data Types
+
+
+ {nodeData.datatype.join(", ")}
+
+
+
+
+
+ Data Standards
+
+
+ {nodeData.standard.join(", ")}
+
+
+
+
+
+ Upstream Contact
+
+
+ {nodeData.support}
+
+
+
+
+
+ NeuroJSON-Cuated Datasets
+
+ {dbInfo ? (
+
+ {dbInfo.doc_count - 1}
+
+ ) : (
+ "Coming soon "
+ )}
+
+
+ {/*database info card*/}
+ {dbInfo ? (
+
+
+
+
+
+ NeuroJSON.io Database Name
+
+ {dbInfo.db_name}
+
+
+
+ REST-API URL
+
+
+
+ {`https://neurojson.io:7777/${dbInfo.db_name}`}
+
+
+
+
+ Database Creation Time
+
+
+ {dateCoverter(dbInfo.instance_start_time)}
+
+
+
+
+ Searchable Database Size
+
+
+ {formatSize(dbInfo.sizes?.external)}
+
+
+
+
+ DatabaseDisk Size (compressed)
+
+ {formatSize(dbInfo.sizes?.file)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+ Select a node to see database info.
+ )}
+ >
+ ) : (
+ Select a node to see database info.
+ )}
+
+
+ );
+};
+
+export default NodeInfoPanel;
diff --git a/src/components/NodesFilter/FilterMenu.tsx b/src/components/NodesFilter/FilterMenu.tsx
new file mode 100644
index 0000000..27c8698
--- /dev/null
+++ b/src/components/NodesFilter/FilterMenu.tsx
@@ -0,0 +1,156 @@
+import KeywordFilter from "./KeywordFilter";
+import ModalitiesFilter from "./ModalitiesFilter";
+import FilterListIcon from "@mui/icons-material/FilterList";
+import {
+ IconButton,
+ Menu,
+ MenuItem,
+ Box,
+ Typography,
+ Divider,
+} from "@mui/material";
+import { Colors } from "design/theme";
+import React, { useState, useEffect } from "react";
+
+interface FilterMenuProps {
+ onKeywordFilter: (query: string) => void;
+ onModalitiesFilter: (selectedModalities: string[]) => void;
+ filterKeyword: string;
+ homeSelectedModalities: string[];
+}
+
+const FilterMenu: React.FC = ({
+ onKeywordFilter,
+ onModalitiesFilter,
+ filterKeyword,
+ homeSelectedModalities,
+}) => {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const [menuKey, setMenuKey] = useState(0); // Forces re-render
+
+ useEffect(() => {
+ const handleResize = () => {
+ setMenuKey((prevKey) => prevKey + 1);
+ };
+
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ // Handle menu open and close
+ const handleClick = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ return (
+
+ {/* Filter Icon Button */}
+
+
+
+
+ {/* Dropdown Menu */}
+
+
+ );
+};
+
+export default FilterMenu;
diff --git a/src/components/NodesFilter/KeywordFilter.tsx b/src/components/NodesFilter/KeywordFilter.tsx
new file mode 100644
index 0000000..1d2b0f6
--- /dev/null
+++ b/src/components/NodesFilter/KeywordFilter.tsx
@@ -0,0 +1,41 @@
+import { TextField } from "@mui/material";
+import React, { useEffect, useState } from "react";
+
+interface FilterSearchProps {
+ onFilter: (query: string) => void;
+ filterKeyword: string;
+}
+
+const KeywordFilter: React.FC = ({
+ onFilter,
+ filterKeyword,
+}) => {
+ const [inputValue, setInputValue] = useState(filterKeyword);
+
+ useEffect(() => {
+ setInputValue(filterKeyword);
+ }, [filterKeyword]);
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ setInputValue(value);
+ onFilter(value); // Pass value to parent component
+ };
+
+ return (
+
+ );
+};
+
+export default KeywordFilter;
diff --git a/src/components/NodesFilter/ModalitiesFilter.tsx b/src/components/NodesFilter/ModalitiesFilter.tsx
new file mode 100644
index 0000000..07f6446
--- /dev/null
+++ b/src/components/NodesFilter/ModalitiesFilter.tsx
@@ -0,0 +1,108 @@
+import {
+ Box,
+ FormControlLabel,
+ Checkbox,
+ Typography,
+ Button,
+} from "@mui/material";
+import { Colors } from "design/theme";
+import { DATA_TYPE_COLORS } from "modules/universe/NeuroJsonGraph";
+import React, { useEffect, useState } from "react";
+
+interface ModalitiesFilterProps {
+ onFilter: (selectedModalities: string[]) => void;
+ homeSeletedModalities: string[]; // add prop to receive parent state
+}
+
+const modalitiesList = ["mri", "pet", "meg", "eeg", "ieeg", "dwi", "fnirs"];
+
+const ModalitiesFilter: React.FC = ({
+ onFilter,
+ homeSeletedModalities,
+}) => {
+ const [selectedModalities, setSelectedModalities] = useState(
+ homeSeletedModalities
+ );
+
+ useEffect(() => {
+ setSelectedModalities(homeSeletedModalities);
+ }, [homeSeletedModalities]);
+
+ const handleModalityChange = (modality: string) => {
+ const updatedModalities = selectedModalities.includes(modality)
+ ? selectedModalities.filter((m) => m !== modality)
+ : [...selectedModalities, modality];
+ setSelectedModalities(updatedModalities);
+ onFilter(updatedModalities);
+ };
+
+ // reset function to clear all selected checkedboxes
+ const handleReset = () => {
+ setSelectedModalities([]); // clear the local state
+ onFilter([]); // notify parent that selection is reset
+ };
+
+ return (
+
+ {modalitiesList.map((modality) => {
+ const bgColor = DATA_TYPE_COLORS[modality]
+ ? `rgb(${DATA_TYPE_COLORS[modality].join(",")})`
+ : "transparent";
+ return (
+
+ handleModalityChange(modality)}
+ />
+ }
+ label={
+
+ {modality}
+
+ }
+ />
+
+ );
+ })}
+ {/* Reset button */}
+
+
+ );
+};
+
+export default ModalitiesFilter;
diff --git a/src/components/PreviewModal.tsx b/src/components/PreviewModal.tsx
new file mode 100644
index 0000000..4a3ce82
--- /dev/null
+++ b/src/components/PreviewModal.tsx
@@ -0,0 +1,343 @@
+///
+import { CircularProgress } from "@mui/material";
+import React, { useEffect, useRef } from "react";
+
+// Tell TS that THREE is available globally
+declare const THREE: typeof import("three");
+
+declare global {
+ interface Window {
+ scene: any;
+ camera: any;
+ renderer: any;
+ previewdata: (...args: any[]) => void;
+ [key: string]: any;
+ }
+}
+
+const PreviewModal: React.FC<{
+ isOpen: boolean;
+ dataKey: any;
+ isInternal: boolean;
+ onClose: () => void;
+ isLoading: boolean; //add spinner
+ previewIndex: number;
+}> = ({ isOpen, dataKey, isInternal, onClose, previewIndex, isLoading }) => {
+ // Create a ref. This will give us a direct, stable reference to the container div. fix start------------
+ // const canvasContainerRef = useRef(null);
+ // fix end---------------------
+
+ useEffect(() => {
+ if (!isOpen) return;
+ //add spinner
+ // if (!isOpen || isLoading) return;
+
+ // fix start-----------: Get the container element from the ref.
+ // const container = canvasContainerRef.current;
+ // if (!container) {
+ // // This can happen briefly on the first render, so we just wait for the next render.
+ // return;
+ // }
+ // // 3. Check for the required legacy functions on the window object.
+ // if (
+ // typeof window.previewdata !== "function" ||
+ // typeof window.initcanvas_with_container !== "function"
+ // ) {
+ // console.error(
+ // "❌ Legacy preview script functions are not available on the window object."
+ // );
+ // return;
+ // }
+
+ // window.previewdata(dataKey, previewIndex, isInternal, false);
+ // fix end---------------------------------
+ // clear old canvas
+ const canvasDiv = document.getElementById("canvas");
+ if (canvasDiv)
+ while (canvasDiv.firstChild) canvasDiv.removeChild(canvasDiv.firstChild);
+
+ // wait until div with ID canvas is in the DOM and also confirm the global function exists
+ const interval = setInterval(() => {
+ if (
+ document.getElementById("canvas") &&
+ typeof window.previewdata === "function"
+ ) {
+ clearInterval(interval);
+ window.previewdata(dataKey, previewIndex, isInternal, false);
+ }
+ }, 50);
+
+ return () => {
+ clearInterval(interval);
+ };
+ }, [isOpen, dataKey, previewIndex, isInternal]);
+
+ if (!isOpen) return null;
+ return (
+
+ {/* Spinner overlay */}
+ {/* {isLoading && (
+
+ )} */}
+
+ {/* Canvas Panel */}
+
+ {/* --- NEW: Conditional rendering of the spinner --- */}
+ {/* {isLoading && } */}
+
+ {/*
+ {isLoading && }
+
*/}
+
+ {/* Control Panel */}
+
+
+ );
+};
+
+export default PreviewModal;
diff --git a/src/components/Routes.tsx b/src/components/Routes.tsx
index a2dc9f2..b1918ee 100644
--- a/src/components/Routes.tsx
+++ b/src/components/Routes.tsx
@@ -1,15 +1,44 @@
import FullScreen from "design/Layouts/FullScreen";
+import AboutPage from "pages/AboutPage";
+import DatabasePage from "pages/DatabasePage";
+import DatasetDetailPage from "pages/DatasetDetailPage";
+import DatasetPage from "pages/DatasetPage";
import Home from "pages/Home";
-import { Navigate, Route, Routes as RoutesFromRouter } from "react-router-dom";
+import SearchPage from "pages/SearchPage";
+import NewDatasetPage from "pages/UpdatedDatasetPage";
+import React from "react";
+import { Navigate, Route, Routes as RouterRoutes } from "react-router-dom";
import RoutesEnum from "types/routes.enum";
const Routes = () => (
-
- }>
- } />
-
+
+ {/* FullScreen Layout */}
+ }>
+ {/* Home Page */}
+ } />
+ {/* Databases Page */}
+ } />
- } />
-
+ {/* Dataset List Page */}
+ }
+ element={}
+ />
+
+ {/* Dataset Details Page */}
+ }
+ />
+
+ {/* Search Page */}
+ } />
+
+ {/* About Page */}
+ } />
+
+
);
+
export default Routes;
diff --git a/src/components/SearchPage/DatasetCard.tsx b/src/components/SearchPage/DatasetCard.tsx
new file mode 100644
index 0000000..110b6fc
--- /dev/null
+++ b/src/components/SearchPage/DatasetCard.tsx
@@ -0,0 +1,214 @@
+import { Typography, Card, CardContent, Stack, Chip } from "@mui/material";
+import { Colors } from "design/theme";
+import React from "react";
+import { Link } from "react-router-dom";
+import RoutesEnum from "types/routes.enum";
+
+interface DatasetCardProps {
+ dbname: string;
+ dsname: string;
+ parsedJson: {
+ key: string;
+ value: {
+ name?: string;
+ readme?: string;
+ modality?: string[];
+ subj?: string[];
+ info?: {
+ Authors?: string[];
+ DatasetDOI?: string;
+ };
+ };
+ };
+ index: number;
+ onChipClick: (key: string, value: string) => void;
+ keyword?: string; // for keyword highlight
+}
+
+const DatasetCard: React.FC = ({
+ dbname,
+ dsname,
+ parsedJson,
+ index,
+ onChipClick,
+ keyword,
+}) => {
+ const { name, readme, modality, subj, info } = parsedJson.value;
+ const datasetLink = `${RoutesEnum.DATABASES}/${dbname}/${dsname}`;
+
+ // prepare DOI URL
+ const rawDOI = info?.DatasetDOI?.replace(/^doi:/, "");
+ const doiLink = rawDOI ? `https://doi.org/${rawDOI}` : null;
+
+ // keyword hightlight functional component
+ const highlightKeyword = (text: string, keyword?: string) => {
+ if (!keyword || !text?.toLowerCase().includes(keyword.toLowerCase())) {
+ return text;
+ }
+
+ const regex = new RegExp(`(${keyword})`, "gi"); // for case-insensitive and global
+ const parts = text.split(regex);
+
+ return (
+ <>
+ {parts.map((part, i) =>
+ part.toLowerCase() === keyword.toLowerCase() ? (
+
+ {part}
+
+ ) : (
+ {part}
+ )
+ )}
+ >
+ );
+ };
+
+ return (
+
+
+ {/* card number in bottom-right */}
+
+ #{index + 1}
+
+
+
+ {highlightKeyword(name || "Untitled Dataset", keyword)}
+
+
+ Database: {dbname} | Dataset: {dsname}
+
+
+
+
+
+ Modalities:
+
+
+ {Array.isArray(modality) && modality.length > 0 ? (
+ modality.map((mod, idx) => (
+ onChipClick("modality", mod)}
+ sx={{
+ "& .MuiChip-label": {
+ paddingX: "6px",
+ fontSize: "0.8rem",
+ },
+ height: "24px",
+ color: Colors.darkPurple,
+ border: `1px solid ${Colors.darkPurple}`,
+ fontWeight: "bold",
+ transition: "all 0.2s ease",
+ "&:hover": {
+ backgroundColor: `${Colors.purple} !important`,
+ color: "white",
+ borderColor: Colors.purple,
+ },
+ }}
+ />
+ ))
+ ) : (
+
+ N/A
+
+ )}
+
+
+
+
+ Subjects: {subj && `${subj.length} subjects`}
+
+
+
+ {readme && (
+
+ Summary: {highlightKeyword(readme, keyword)}
+
+ )}
+
+
+ {info?.Authors?.length && (
+
+ {info?.Authors && (
+
+ Authors:{" "}
+ {highlightKeyword(
+ Array.isArray(info.Authors)
+ ? info.Authors.join(", ")
+ : typeof info.Authors === "string"
+ ? info.Authors
+ : "N/A",
+ keyword
+ )}
+
+ )}
+
+ )}
+
+
+
+ {doiLink && (
+
+
+
+ )}
+
+
+
+
+ );
+};
+
+export default DatasetCard;
diff --git a/src/components/SearchPage/SubjectCard.tsx b/src/components/SearchPage/SubjectCard.tsx
new file mode 100644
index 0000000..3700086
--- /dev/null
+++ b/src/components/SearchPage/SubjectCard.tsx
@@ -0,0 +1,230 @@
+import PersonOutlineIcon from "@mui/icons-material/PersonOutline";
+import { Typography, Card, CardContent, Stack, Chip } from "@mui/material";
+import { Colors } from "design/theme";
+import React from "react";
+import { Link } from "react-router-dom";
+import RoutesEnum from "types/routes.enum";
+
+interface SubjectCardProps {
+ dbname: string;
+ dsname: string;
+ age: string;
+ subj: string;
+ parsedJson: {
+ key: string[];
+ value: {
+ modalities?: string[];
+ tasks?: string[];
+ sessions?: string[];
+ types?: string[];
+ };
+ };
+ index: number;
+ onChipClick: (key: string, value: string) => void;
+}
+
+const SubjectCard: React.FC = ({
+ dbname,
+ dsname,
+ age,
+ subj,
+ parsedJson,
+ index,
+ onChipClick,
+}) => {
+ const { modalities, tasks, sessions, types } = parsedJson.value;
+ const subjectLink = `${RoutesEnum.DATABASES}/${dbname}/${dsname}`;
+
+ // get the gender of subject
+ const genderCode = parsedJson?.key?.[1];
+ let genderDisplay = "Unknown";
+
+ if (genderCode) {
+ if (genderCode === "000F") genderDisplay = "Female";
+ else if (genderCode === "000M") genderDisplay = "Male";
+ }
+
+ // cover age string to readable format
+ let ageDisplay = "N/A";
+ if (age) {
+ const ageNum = parseInt(age, 10) / 100;
+ if (Number.isInteger(ageNum)) {
+ ageDisplay = `${ageNum} years`;
+ } else {
+ ageDisplay = `${ageNum.toFixed(1)} years`;
+ }
+ }
+
+ return (
+
+
+ {/* card number in bottom-right */}
+
+ #{index + 1}
+
+
+
+
+ Subject: {subj} | Dataset: {dsname}
+
+
+
+ Age: {ageDisplay} | Gender: {genderDisplay}{" "}
+ | Database: {dbname}
+
+
+
+
+
+ Modalities:
+
+ {modalities?.map((mod, idx) => (
+ onChipClick("modality", mod)} //
+ sx={{
+ "& .MuiChip-label": {
+ paddingX: "6px",
+ fontSize: "0.8rem",
+ },
+ height: "24px",
+ color: Colors.darkPurple,
+ border: `1px solid ${Colors.darkPurple}`,
+ fontWeight: "bold",
+ transition: "all 0.2s ease",
+ "&:hover": {
+ backgroundColor: `${Colors.purple} !important`,
+ color: "white",
+ borderColor: Colors.purple,
+ },
+ }}
+ />
+ ))}
+
+
+
+
+ Tasks:
+
+ {Array.isArray(tasks) && tasks.length > 0 ? (
+ tasks.map((task, idx) => (
+ onChipClick("task_name", task)}
+ sx={{
+ "& .MuiChip-label": {
+ paddingX: "6px",
+ fontSize: "0.8rem",
+ },
+ height: "24px",
+ color: Colors.darkPurple,
+ border: `1px solid ${Colors.darkPurple}`,
+ fontWeight: "bold",
+ transition: "all 0.2s ease",
+ "&:hover": {
+ backgroundColor: `${Colors.purple} !important`,
+ color: "white",
+ borderColor: Colors.purple,
+ },
+ }}
+ />
+ ))
+ ) : (
+
+ N/A
+
+ )}
+
+
+
+
+ Types:
+
+ {Array.isArray(types) && types.length > 0 ? (
+ types.map((type, idx) => (
+ onChipClick("type_name", type)}
+ sx={{
+ "& .MuiChip-label": {
+ paddingX: "6px",
+ fontSize: "0.8rem",
+ },
+ height: "24px",
+ color: Colors.darkPurple,
+ border: `1px solid ${Colors.darkPurple}`,
+ fontWeight: "bold",
+ transition: "all 0.2s ease",
+ "&:hover": {
+ backgroundColor: `${Colors.purple} !important`,
+ color: "white",
+ borderColor: Colors.purple,
+ },
+ }}
+ />
+ ))
+ ) : (
+
+ N/A
+
+ )}
+
+
+
+ Sessions: {sessions?.length}
+
+
+
+
+
+ );
+};
+
+export default SubjectCard;
diff --git a/src/components/StatisticsBanner.tsx b/src/components/StatisticsBanner.tsx
new file mode 100644
index 0000000..204ad70
--- /dev/null
+++ b/src/components/StatisticsBanner.tsx
@@ -0,0 +1,236 @@
+import ContentPasteSearchIcon from "@mui/icons-material/ContentPasteSearch";
+import DatasetLinkedIcon from "@mui/icons-material/DatasetLinked";
+import PeopleAltIcon from "@mui/icons-material/PeopleAlt";
+import StorageIcon from "@mui/icons-material/Storage";
+import TopicIcon from "@mui/icons-material/Topic";
+import { Box, Typography } from "@mui/material";
+import { Colors } from "design/theme";
+import { useAppDispatch } from "hooks/useAppDispatch";
+import { useAppSelector } from "hooks/useAppSelector";
+import React, { useEffect } from "react";
+import { fetchDbStats } from "redux/neurojson/neurojson.action";
+import { DbStatsItem } from "redux/neurojson/types/neurojson.interface";
+import { RootState } from "redux/store";
+
+// function for calculate links and size
+const calculateLinksAndSize = (dbStats: DbStatsItem[] | null) => {
+ if (!dbStats) return { totalLinks: 0, totalSizeTB: "0.00" };
+
+ const filtered = dbStats.filter(
+ (item) => item.view !== "dbinfo" && item.view !== "subjects"
+ );
+
+ const totalLinks = filtered.reduce((acc, item) => acc + item.num, 0);
+ const totalSizeBytes = filtered.reduce((acc, item) => acc + item.size, 0);
+ const totalSizeTB = Math.floor(totalSizeBytes / 1024 ** 4);
+ return { totalLinks, totalSizeTB };
+};
+
+const StatisticsBanner: React.FC = () => {
+ const dispatch = useAppDispatch();
+ const dbstats = useAppSelector((state: RootState) => state.neurojson.dbStats);
+ const registry = useAppSelector(
+ (state: RootState) => state.neurojson.registry
+ );
+
+ const databaseCount = registry?.length ?? "-";
+ const datasetStat = dbstats?.find((item) => item.view === "dbinfo");
+ const subjectStat = dbstats?.find((item) => item.view === "subjects");
+ const { totalLinks, totalSizeTB } = calculateLinksAndSize(dbstats);
+
+ // format numbers with commas
+ const formatNumber = (num: number | undefined) =>
+ num?.toLocaleString() ?? "—";
+
+ useEffect(() => {
+ dispatch(fetchDbStats());
+ }, [dispatch]);
+
+ return (
+
+ {/* Databases */}
+
+
+
+
+ {databaseCount.toLocaleString()}
+
+
+ Databases
+
+
+
+
+ {/* Datasets */}
+
+
+
+
+ {formatNumber(datasetStat?.num)}
+
+
+ Datasets
+
+
+
+ {/* Subjects */}
+
+
+
+
+ {formatNumber(subjectStat?.num)}
+
+
+ Subjects
+
+
+
+ {/* Links */}
+
+
+
+
+ {totalLinks.toLocaleString() ?? "-"}
+
+
+ Links
+
+
+
+ {/* Size */}
+
+
+
+
+ {totalSizeTB ?? "-"} TB
+
+
+ Size
+
+
+
+
+ );
+};
+
+export default StatisticsBanner;
diff --git a/src/components/WorkflowDetailCard.tsx b/src/components/WorkflowDetailCard.tsx
new file mode 100644
index 0000000..ebaa3a2
--- /dev/null
+++ b/src/components/WorkflowDetailCard.tsx
@@ -0,0 +1,103 @@
+import {
+ Typography,
+ Card,
+ CardContent,
+ Box,
+ Button,
+ CardMedia,
+} from "@mui/material";
+import { Colors } from "design/theme";
+import React from "react";
+
+interface WorkflowDetailCardProps {
+ type: "data-portals" | "json-conversion" | "database" | "rest-api";
+}
+
+const cardContentMap = {
+ "data-portals": {
+ title: "Data Portals",
+ backgroundColor: "#99aff0",
+ color: Colors.darkPurple,
+ description: `NeuroJSON.io curates reusable neuroimaging datasets with searchable and standardized metadata. It hosts thousands of BIDS compatible datasets with diverse imaging modalities, including MRI, fMRI, EEG, MEG, fNIRS measurements and processed data such as segmentation, surface and tetrahedral meshes.`,
+ },
+ "json-conversion": {
+ title: "JSON Conversion",
+ backgroundColor: "#384979",
+ color: Colors.lightGray,
+ description: `NeuroJSON project adopts internationally standardized JSON and binary JSON formats as the unified data exchange format for complex scientific data. Using JSON for storing and exchanging neuroimaging data not only make the dataset cloud- and web-ready, but also reinforces human-readability - the key to ensure long-term reusability of datasets.`,
+ },
+ database: {
+ title: "Scalable Neuroimaging Database",
+ backgroundColor: "#ffd230",
+ color: Colors.darkPurple,
+ description: `NeuroJSON.io utilizes industrial standard NoSQL databases and JSON data exchange format to store, index, and query large-scale neuroimaging datasets, accommodating rapid growth in the needs for emerging machine learning based data analyses. It converts all searchable metadata in the form of lightweight JSON format, making the data easy to reuse and understood.`,
+ },
+ "rest-api": {
+ title: "REST-API Data Access",
+ backgroundColor: "#3533cd",
+ color: Colors.lightGray,
+ description: `Every dataset hosted on NeuroJSON.io can be accessed via the REST-APIs - a URL-based universal data retrieval and manipulation method, making it possible to dynamically query, download, and analyze large-scale neuroimaging datasets inside diverse programming environments such as MATLAB, Octave and Python.`,
+ },
+};
+
+const WorkflowDetailCard: React.FC = ({ type }) => {
+ const { title, description, backgroundColor, color } = cardContentMap[type];
+
+ return (
+
+ {/* Image on top */}
+ {/* */}
+
+
+
+ {title}
+
+
+
+ {/* content */}
+
+
+ {description}
+
+
+
+
+
+
+ );
+};
+
+export default WorkflowDetailCard;
diff --git a/src/declare.d.ts b/src/declare.d.ts
index e238d01..de93053 100644
--- a/src/declare.d.ts
+++ b/src/declare.d.ts
@@ -1,2 +1,47 @@
-declare module '*.png';
-declare module '*.svg';
+declare module "*.png";
+declare module "*.svg";
+
+declare global {
+ interface Window {
+ dopreview: (
+ key: any,
+ idx: number,
+ isinternal: boolean,
+ hastime: any[]
+ ) => void;
+ previewdata: any;
+ previewdataurl: any;
+ initcanvas: () => void;
+ drawpreview: (data: any) => void;
+ update: () => void;
+ createStats: () => any;
+ setControlAngles: (angles: any) => void;
+ setcrosssectionsizes: (sizes: any) => void;
+ }
+}
+
+declare module "react-syntax-highlighter";
+declare module "react-syntax-highlighter/dist/esm/styles/hljs";
+declare module "react-syntax-highlighter/dist/esm/languages/hljs/*";
+
+// declare module "./utils/preview.js" {
+// export function dopreview(
+// key: any,
+// idx: number,
+// isinternal?: boolean,
+// hastime?: any
+// ): void;
+// export function initcanvas(): void;
+// export function drawpreview(data: any): void;
+// export function update(): void;
+// }
+
+// declare module "jda" {
+// const jdata: any;
+// export default jdata;
+// }
+
+// declare module "bjd" {
+// const bjdata: any;
+// export default bjdata;
+// }
diff --git a/src/design/Layouts/FullScreen.tsx b/src/design/Layouts/FullScreen.tsx
index 4587bb9..4f94f9a 100644
--- a/src/design/Layouts/FullScreen.tsx
+++ b/src/design/Layouts/FullScreen.tsx
@@ -1,218 +1,47 @@
-import { AppBar, Box, Grid, Toolbar, Typography } from "@mui/material";
-import { Colors } from "design/theme";
-import useIsLargeScreen from "hooks/useIsLargeScreen";
-import { Outlet, useNavigate } from "react-router-dom";
+import { Box } from "@mui/material";
+import DefaultNavBar from "components/NavBar/DefaultNavBar";
+import LandingNavBar from "components/NavBar/LandingNavBar";
+import { useState, useEffect } from "react";
+import { Outlet, useLocation } from "react-router-dom";
const FullScreen = () => {
- const isLargeScreen = useIsLargeScreen();
- const navigate = useNavigate();
-
- const justifyContentValue = isLargeScreen ? "flex-start" : "space-between";
-
- return (
- <>
-
-
-
-
-
-
- NeuroJSON.io
-
-
- Free Data Worth Sharing
-
-
-
-
- {/* Mission */}
-
-
- Mission
-
-
-
- {/* Get Started */}
-
-
- Get Started
-
-
-
- {/* Contribute */}
-
-
- Contribute
-
-
-
- {/* Tools */}
-
-
- Tools
-
-
-
- {/* Search */}
-
-
- Search
-
-
-
- {/* Forum */}
-
-
- Forum
-
-
-
- {/* About */}
-
-
- About
-
-
-
-
-
-
-
-
- >
- );
+ const location = useLocation();
+ const isLandingPage = location.pathname === "/";
+ const [scrolled, setScrolled] = useState(false);
+
+ useEffect(() => {
+ const handleScroll = () => {
+ setScrolled(window.scrollY > window.innerHeight - 100);
+ };
+
+ window.addEventListener("scroll", handleScroll);
+ return () => window.removeEventListener("scroll", handleScroll);
+ }, [isLandingPage]);
+
+ return (
+ <>
+ {isLandingPage ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ >
+ );
};
export default FullScreen;
diff --git a/src/design/theme.ts b/src/design/theme.ts
index 5d507ce..e786d3a 100644
--- a/src/design/theme.ts
+++ b/src/design/theme.ts
@@ -1,62 +1,71 @@
+// import { orange, purple } from "@mui/material/colors";
import { createTheme } from "@mui/material/styles";
const primary = {
- dark: "#5c6386",
- main: "#7b81a5",
- light: "#a0a5c2",
+ dark: "#5c6386",
+ main: "#7b81a5",
+ light: "#a0a5c2",
};
const secondary = {
- dark: "#374056",
- main: "#48556B",
- light: "#7487a0",
+ dark: "#374056",
+ main: "#48556B",
+ light: "#7487a0",
};
export const Colors = {
- white: "#FFFFFF",
- black: "#000000",
- lightGray: "#f2f2f2",
- darkGray: "#4A4A4A",
- accent: "#D5A021",
- success: "#03BB50",
- error: "#D9534F",
- textPrimary: "#212121",
- textSecondary: "#494747",
- primary,
- secondary,
+ white: "#FFFFFF",
+ black: "#000000",
+ lightGray: "#f2f2f2",
+ darkGray: "#4A4A4A",
+ accent: "#D5A021",
+ success: "#03BB50",
+ error: "#D9534F",
+ textPrimary: "#212121",
+ textSecondary: "#494747",
+ green: "#02DEC4",
+ darkGreen: "#49c6ae",
+ yellow: "#FFDD31",
+ lightYellow: "#FAEBD7",
+ purple: "#5865F2",
+ secondaryPurple: "#4a4cb7",
+ darkPurple: "#282C56",
+ orange: "#FF9F2F",
+ darkOrange: "#E88C25",
+ blue: "#1976d2",
+ lightBlue: "#e8f0fe",
+ pink: "#FF69B4",
+ purpleGrey: "#99aff0",
+ primary,
+ secondary,
};
const theme = createTheme({
- typography: {
- fontFamily: [
- '"IBM Plex Sans"',
- "-apple-system",
- "BlinkMacSystemFont",
- '"Segoe UI"',
- "Roboto",
- '"Helvetica Neue"',
- "Arial",
- "sans-serif",
- ].join(","),
- h1: {
- fontWeight: 700,
- fontSize: "2rem",
- color: Colors.primary.dark,
- },
- h2: {
- fontWeight: 600,
- fontSize: "1.75rem",
- color: Colors.secondary.main,
- },
- body1: {
- fontSize: "1rem",
- color: Colors.textPrimary,
- },
- body2: {
- fontSize: "0.875rem",
- color: Colors.textSecondary,
- },
- },
+ typography: {
+ fontFamily: ["Raleway", "Ubuntu"].join(","),
+ h1: {
+ fontFamily: "Raleway",
+ fontWeight: 700,
+ fontSize: "2.5rem",
+ textTransform: "none",
+ },
+ h2: {
+ fontWeight: 600,
+ fontSize: "1.2rem",
+ textTransform: "none",
+ color: Colors.secondary.main,
+ },
+ body1: {
+ fontSize: "1rem",
+ color: Colors.textPrimary,
+ fontFamily: "Ubuntu",
+ },
+ body2: {
+ fontSize: "0.875rem",
+ color: Colors.textSecondary,
+ fontFamily: "Ubuntu",
+ },
+ },
});
export default theme;
diff --git a/src/hooks/useIsLargeScreen.ts b/src/hooks/useIsLargeScreen.ts
index 1e2b034..96c093e 100644
--- a/src/hooks/useIsLargeScreen.ts
+++ b/src/hooks/useIsLargeScreen.ts
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
+
const useIsLargeScreen = () => {
const [isLargeScreen, setIsLargeScreen] = useState(false);
diff --git a/src/index.tsx b/src/index.tsx
index 88f28f8..ee78b23 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,20 +1,40 @@
-import App from "./App";
-import { ThemeProvider } from "@emotion/react";
-import { CssBaseline } from "@mui/material";
-import theme from "design/theme";
+import React from "react";
import ReactDOM from "react-dom/client";
+import App from "./App";
import { Provider } from "react-redux";
-import store from "redux/store";
+import store from "./redux/store";
+import { ThemeProvider } from "@mui/material/styles";
+import { CssBaseline } from "@mui/material";
+import theme from "./design/theme";
+import * as preview from "./utils/preview.js";
-const root = ReactDOM.createRoot(
- document.getElementById("root") as HTMLElement
-);
+import { previewdataurl, previewdata, dopreview, drawpreview, update, initcanvas, createStats, setControlAngles, setcrosssectionsizes } from "./utils/preview.js";
+
+// Get the root element
+const rootElement = document.getElementById("root") as HTMLElement;
+
+// Create the root for rendering React
+const root = ReactDOM.createRoot(rootElement);
+
+// Expose dopreview globally
+(window as any).previewdataurl = previewdataurl;
+(window as any).previewdata = previewdata;
+(window as any).dopreview = dopreview;
+(window as any).drawpreview = drawpreview;
+(window as any).initcanvas = initcanvas;
+(window as any).update = update;
+(window as any).createStats = createStats;
+(window as any).setControlAngles = setControlAngles;
+(window as any).setcrosssectionsizes = setcrosssectionsizes;
+// Render the application
root.render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
diff --git a/src/modules/universe/ForceGraphModal.tsx b/src/modules/universe/ForceGraphModal.tsx
new file mode 100644
index 0000000..fb81c44
--- /dev/null
+++ b/src/modules/universe/ForceGraphModal.tsx
@@ -0,0 +1,129 @@
+import NeuroJsonGraph from "./NeuroJsonGraph";
+import { NodeObject } from "./NeuroJsonGraph";
+import CloseIcon from "@mui/icons-material/Close";
+import {
+ Modal,
+ Box,
+ IconButton,
+ Button,
+ Typography,
+ CircularProgress,
+} from "@mui/material";
+import FilterMenu from "components/NodesFilter/FilterMenu";
+import { Colors } from "design/theme";
+import React, { useState } from "react";
+
+interface ForceGraphModalProps {
+ registry: any[] | null;
+ filteredRegistry: any[];
+ filterKeyword: string;
+ selectedModalities: string[];
+ setFilterKeyword: (keyword: string) => void;
+ setSelectedModalities: (modalities: string[]) => void;
+ onNodeClick: (node: NodeObject) => void;
+}
+
+const ForceGraphModal: React.FC = ({
+ registry,
+ filteredRegistry,
+ filterKeyword,
+ selectedModalities,
+ setFilterKeyword,
+ setSelectedModalities,
+ onNodeClick,
+}) => {
+ const [open, setOpen] = useState(false);
+
+ return (
+ <>
+
+ setOpen(false)}>
+
+ setOpen(false)}
+ sx={{
+ position: "absolute",
+ top: 20,
+ right: 20,
+ color: "white",
+ zIndex: 9999,
+ }}
+ >
+
+
+
+ {/* Filter Menu Button */}
+
+
+
+
+ {/* 3D graph */}
+
+ {!registry ? (
+
+
+
+ ) : filteredRegistry.length > 0 ? (
+
+ ) : (
+
+
+ No matching nodes found
+
+
+ )}
+
+
+
+ >
+ );
+};
+
+export default ForceGraphModal;
diff --git a/src/modules/universe/NeuroJsonGraph.tsx b/src/modules/universe/NeuroJsonGraph.tsx
index ce732fa..df48be7 100644
--- a/src/modules/universe/NeuroJsonGraph.tsx
+++ b/src/modules/universe/NeuroJsonGraph.tsx
@@ -1,104 +1,294 @@
import ForceGraph3D from "3d-force-graph";
import { Colors } from "design/theme";
-import { useAppDispatch } from "hooks/useAppDispatch";
-import { useAppSelector } from "hooks/useAppSelector";
import React, { useEffect, useRef } from "react";
-import { fetchRegistry } from "redux/neurojson/neurojson.action";
-import { NeurojsonSelector } from "redux/neurojson/neurojson.selector";
+import { useNavigate } from "react-router-dom";
import * as THREE from "three";
import {
- CSS2DObject,
- CSS2DRenderer,
+ CSS2DObject,
+ CSS2DRenderer,
} from "three/examples/jsm/renderers/CSS2DRenderer";
import { Database } from "types/responses/registry.interface";
-interface NodeObject {
- id: string;
- name: string;
- dbname: string;
- color: string;
- datatype: string[];
- support: string;
- url: string;
+export interface NodeObject {
+ id: string;
+ name: string;
+ dbname: string;
+ color: string;
+ datatype: string[];
+ support: string;
+ url: string;
+ datasets: number;
+ standard: string[];
}
-const NeuroJsonGraph: React.FC<{ registry: Database[] }> = ({ registry }) => {
- const graphRef = useRef(null);
-
- const { loading, error } = useAppSelector(NeurojsonSelector);
-
- useEffect(() => {
- if (graphRef.current && registry) {
- const graphData = {
- nodes: registry.map((db: Database) => ({
- id: db.id,
- name: db.fullname || db.name,
- dbname: db.name,
- color: "rgba(255,255,255,0.8)",
- datatype: db.datatype,
- support: db.support,
- url: db.url,
- })),
- links: [],
- };
-
- const Graph = ForceGraph3D()(graphRef.current)
- .graphData(graphData)
- .nodeRelSize(2)
- .nodeColor(
- (node) => (node as NodeObject).color || "rgba(255,255,255,0.8)"
- )
- .linkWidth(1)
- .backgroundColor(Colors.primary.light)
- .nodeLabel("name")
- .nodeThreeObject((node) => {
- const nodeEl = document.createElement("span");
- const castNode = node as NodeObject;
- nodeEl.textContent = castNode.dbname || "Unnamed";
- nodeEl.className = "orglabel";
- return new CSS2DObject(nodeEl);
- });
-
- // Initialize CSS2DRenderer for 2D labels
- const labelRenderer = new CSS2DRenderer();
- labelRenderer.setSize(window.innerWidth, window.innerHeight);
- labelRenderer.domElement.style.position = "absolute";
- labelRenderer.domElement.style.top = "0px";
- graphRef.current?.appendChild(labelRenderer.domElement);
-
- // Simple render without overriding WebGLRenderer behavior
- Graph.renderer().domElement.addEventListener("render", (e) => {
- labelRenderer.render(Graph.scene(), Graph.camera());
- });
-
- // Handle window resize
- const resizeGraph = () => {
- Graph.width(window.innerWidth).height(window.innerHeight);
- labelRenderer.setSize(window.innerWidth, window.innerHeight);
- };
- resizeGraph();
- window.addEventListener("resize", resizeGraph);
-
- return () => {
- window.removeEventListener("resize", resizeGraph);
- };
- }
- }, [registry]);
-
- if (loading) return Loading...
;
- if (error) return Error: {error}
;
-
- return (
-
- );
+// Define the datatype to color mapping
+export const DATA_TYPE_COLORS: Record = {
+ mri: [160, 138, 233], // soft laender
+ anat: [160, 138, 233],
+ fmri: [152, 202, 32], // bright lime green
+ func: [152, 202, 32],
+ pet: [0, 105, 192], // deep blue
+ meg: [156, 57, 0], // dark reddish-brown
+ eeg: [134, 31, 55], // dark red-pink
+ ieeg: [18, 109, 62], // forest green
+ beh: [12, 93, 210], // bright blue
+ fmap: [255, 255, 59], // vivid yellow
+ dwi: [200, 9, 12], // deep red
+ fnirs: [255, 193, 7], // golden yellow
+ phenotype: [255, 87, 34], // vibrant orange-red
};
+const NeuroJsonGraph: React.FC<{
+ registry: Database[];
+ onNodeClick?: (node: NodeObject) => void;
+}> = React.memo(({ registry, onNodeClick }) => {
+ const navigate = useNavigate();
+ const graphRef = useRef(null);
+
+ // Function to blend colors based on datatypes
+ const blendColors = (datatypes: string[]): string => {
+ if (datatypes.length === 0) return "rgb(255,255,255)"; // Default white
+
+ let totalR = 0,
+ totalG = 0,
+ totalB = 0;
+ let count = 0;
+
+ datatypes.forEach((type) => {
+ const color = DATA_TYPE_COLORS[type];
+ if (color) {
+ totalR += color[0];
+ totalG += color[1];
+ totalB += color[2];
+ count++;
+ }
+ });
+
+ if (count === 0) count = 1; // Prevent division by zero
+
+ let avgR = Math.floor(totalR / count);
+ let avgG = Math.floor(totalG / count);
+ let avgB = Math.floor(totalB / count);
+
+ return `rgb(${avgR}, ${avgG}, ${avgB})`;
+ };
+
+ // Custom random number generator for link connection usage
+ const mulberry32 = (a: number) => {
+ return function () {
+ let t = (a += 0x6d2b79f5);
+ t = Math.imul(t ^ (t >>> 15), t | 1);
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+ };
+ };
+
+ const rngfun = mulberry32(0x123456789);
+
+ useEffect(() => {
+ // Ensure registry and graphRef are properly initialized
+ if (!registry || registry.length === 0) {
+ console.error("Registry is empty or undefined:", registry);
+ return;
+ }
+
+ if (!graphRef.current) {
+ console.error("Graph ref is null");
+ return;
+ }
+
+ // create a colorlist after blend colors for nodes
+ let colorlist: { brightness: number; index: number; color: string }[] =
+ registry.map((db, index) => {
+ const colorStr = blendColors(db.datatype); // Get color in "rgb(R,G,B)" format
+ const match = colorStr.match(/\d+/g); // Get numbers from "rgb(R,G,B)"
+ if (!match)
+ return { brightness: 255, index, color: "rgb(255, 255, 255)" }; // Default to white if extraction fails
+
+ const [r, g, b] = match.map(Number); // Convert to numbers
+ const brightness = 0.2126 * r + 0.7152 * g + 0.0722 * b; // Compute brightness
+
+ return { brightness, index, color: colorStr };
+ });
+
+ // Sort nodes by brightness
+ colorlist.sort((a, b) => a.brightness - b.brightness);
+
+ // Prepare graph data
+ const graphData = {
+ nodes: registry.map((db) => {
+ const color = blendColors(db.datatype);
+ let size =
+ db.datasets > 100 ? Math.log(db.datasets) * 2.5 : db.datasets / 7;
+ size = Math.max(size, 4);
+
+ return {
+ id: db.id,
+ name: db.fullname || db.name,
+ dbname: db.name,
+ color: color,
+ datatype: db.datatype,
+ support: db.support,
+ url: db.url,
+ datasets: db.datasets,
+ size: size,
+ standard: db.standard || [],
+ };
+ }),
+
+ links: colorlist.flatMap(({ index, color }, colorIdx) => {
+ const node = registry[index];
+ // Determine number of connections
+ const scaledDatasets =
+ node.datasets > 100 ? Math.log(node.datasets) : node.datasets;
+ const conn =
+ 1 + Math.round(rngfun() * Math.max(1, scaledDatasets / 20));
+
+ const connections: {
+ source: string;
+ target: string;
+ color: string;
+ visible: boolean;
+ }[] = [];
+
+ for (let j = -conn; j <= conn; j++) {
+ if (j === 0) continue;
+ const targetColorIdx = colorIdx + j;
+ if (targetColorIdx < 0 || targetColorIdx >= colorlist.length)
+ continue; // Prevent out-of-bounds errors
+ const targetIdx = colorlist[targetColorIdx].index; // Get registry node index in colorlist order
+ const targetNode = registry[targetIdx]; // Get target node info in registry
+
+ connections.push({
+ source: node.id,
+ target: targetNode.id,
+ color: blendColors(node.datatype),
+ visible: true,
+ });
+ }
+
+ return connections;
+ }),
+ };
+
+ // Initialize 3D Force Graph
+ const Graph = new ForceGraph3D(graphRef.current)
+ .graphData(graphData)
+ .nodeRelSize(1)
+ .nodeColor((node) => (node as NodeObject).color)
+ .linkVisibility(() => false)
+ .linkWidth(1)
+ .backgroundColor("rgba(0,0,0,0)")
+ .nodeLabel("name")
+ .onNodeHover((node) => {
+ // Change cursor on hover
+ graphRef.current!.style.cursor = node ? "pointer" : "default";
+ })
+ .onNodeClick((node) => {
+ const castNode = node as NodeObject;
+ if (onNodeClick) {
+ onNodeClick(castNode);
+ }
+ })
+ .nodeThreeObject((node) => {
+ const castNode = node as NodeObject;
+
+ // Create a group to hold sphere and glow
+ const group = new THREE.Group();
+
+ // Create a 3D sphere for the node
+ const sphereGeometry = new THREE.SphereGeometry(
+ (castNode as any).size,
+ 16,
+ 16
+ );
+ const sphereMaterial = new THREE.MeshBasicMaterial({
+ color: (castNode as any).color,
+ });
+ const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
+ group.add(sphere);
+
+ // Create glow effect
+ const glowGeometry = new THREE.SphereGeometry(
+ (castNode as any).size * 1.2,
+ 16,
+ 16
+ );
+ const glowMaterial = new THREE.MeshBasicMaterial({
+ color: (castNode as any).color,
+ transparent: true,
+ opacity: 0.5,
+ });
+ const glow = new THREE.Mesh(glowGeometry, glowMaterial);
+
+ // Animate glow
+ const animate = () => {
+ glow.scale.setScalar(1 + Math.sin(Date.now() * 0.003) * 0.1);
+ requestAnimationFrame(animate);
+ };
+ animate();
+
+ group.add(glow);
+
+ // Add label as CSS2DObject
+ const label = new CSS2DObject(document.createElement("div"));
+ label.element.textContent = castNode.dbname || "Unnamed";
+ label.element.style.color = Colors.lightGray;
+ label.element.style.fontSize = "16px";
+ label.element.style.pointerEvents = "none";
+ label.position.set(0, 10, 0);
+ group.add(label);
+
+ return group;
+ });
+
+ Graph.d3Force("link")?.distance(100);
+ Graph.cameraPosition({ z: 1000 }); // Adjust zoom level
+
+ // Initialize CSS2DRenderer for 2D labels
+ const labelRenderer = new CSS2DRenderer();
+ labelRenderer.setSize(window.innerWidth, window.innerHeight);
+ labelRenderer.domElement.style.position = "absolute";
+ labelRenderer.domElement.style.top = "0px";
+ labelRenderer.domElement.style.pointerEvents = "none";
+ graphRef.current?.appendChild(labelRenderer.domElement);
+
+ // Animate label rendering
+ const animate = () => {
+ requestAnimationFrame(animate);
+ labelRenderer.render(Graph.scene(), Graph.camera());
+ };
+ animate();
+
+ // Handle window resize
+ const resizeGraph = () => {
+ Graph.width(window.innerWidth).height(window.innerHeight);
+ labelRenderer.setSize(window.innerWidth, window.innerHeight);
+ };
+ resizeGraph();
+ window.addEventListener("resize", resizeGraph);
+
+ // Cleanup on component unmount
+ return () => {
+ window.removeEventListener("resize", resizeGraph);
+ if (graphRef.current) {
+ graphRef.current.removeChild(labelRenderer.domElement);
+ }
+ };
+ }, [registry]);
+
+ return (
+
+ );
+});
+
export default NeuroJsonGraph;
diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx
new file mode 100644
index 0000000..7251d85
--- /dev/null
+++ b/src/pages/AboutPage.tsx
@@ -0,0 +1,244 @@
+import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight";
+import {
+ Box,
+ Typography,
+ Container,
+ Button,
+ Grid,
+ Tooltip,
+} from "@mui/material";
+import { Colors } from "design/theme";
+import React, { useRef } from "react";
+
+const videoData = [
+ {
+ src: "search.png",
+ alt: "search icon",
+ tip: "Search tutotial video",
+ video: "preview_video.mp4",
+ // ref: searchVideoRef,
+ },
+ { src: "preview.png", alt: "preview icon", tip: "Preview tutotial video" },
+ { src: "download.png", alt: "download icon", tip: "Download tutotial video" },
+ { src: "api.png", alt: "api icon", tip: "Restful API tutotial video" },
+];
+
+const AboutPage: React.FC = () => {
+ const searchVideoRef = useRef(null);
+ const previewVideoRef = useRef(null);
+ const downloadVideoRef = useRef(null);
+ const apiVideoRef = useRef(null);
+
+ const videoData = [
+ {
+ src: "search.png",
+ alt: "search icon",
+ tip: "Search tutotial video",
+ video: "preview_video.mp4",
+ ref: searchVideoRef,
+ },
+ {
+ src: "preview.png",
+ alt: "preview icon",
+ tip: "Preview tutotial video",
+ video: "preview_video.mp4",
+ ref: previewVideoRef,
+ },
+ {
+ src: "download.png",
+ alt: "download icon",
+ tip: "Download tutotial video",
+ video: "preview_video.mp4",
+ ref: downloadVideoRef,
+ },
+ {
+ src: "api.png",
+ alt: "api icon",
+ tip: "Restful API tutotial video",
+ video: "preview_video.mp4",
+ ref: searchVideoRef,
+ },
+ ];
+
+ return (
+
+ {/*section 1 */}
+
+
+
+ {/* Left: Text Content */}
+
+
+
+ About NeuroJSON.io
+
+
+ NeuroJSON aims to develop JSON-based neuroimaging data
+ exchange formats that are readable, searchable, shareable, can
+ be readily validated and served in the web and cloud.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* icons */}
+
+ {videoData.map(({ src, alt, tip }) => (
+
+
+
+ ))}
+
+
+
+ {/* section 2*/}
+
+
+
+ Getting Started with NeuroJSON
+
+
+
+
+
+
+
+ );
+};
+
+export default AboutPage;
diff --git a/src/pages/DatabasePage.tsx b/src/pages/DatabasePage.tsx
new file mode 100644
index 0000000..70923d8
--- /dev/null
+++ b/src/pages/DatabasePage.tsx
@@ -0,0 +1,105 @@
+import { Box, Typography, Button, Container } from "@mui/material";
+import { Colors } from "design/theme";
+import { useAppDispatch } from "hooks/useAppDispatch";
+import { useAppSelector } from "hooks/useAppSelector";
+import React, { useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import { fetchRegistry } from "redux/neurojson/neurojson.action";
+import { NeurojsonSelector } from "redux/neurojson/neurojson.selector";
+import RoutesEnum from "types/routes.enum";
+
+const DatabasePage: React.FC = () => {
+ const navigate = useNavigate();
+ const dispatch = useAppDispatch();
+ const { registry } = useAppSelector(NeurojsonSelector);
+
+ useEffect(() => {
+ dispatch(fetchRegistry());
+ }, [dispatch]);
+
+ if (!registry || !Array.isArray(registry) || registry.length === 0) {
+ return (
+
+
+
+ No Databases Found
+
+
+ Please check back later or contact support if this persists.
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Databases
+
+
+ {registry.map((db) => {
+ if (!db?.id) {
+ console.warn("Database entry missing ID:", db);
+ return null;
+ }
+
+ return (
+
+ );
+ })}
+
+
+
+ );
+};
+
+export default DatabasePage;
diff --git a/src/pages/DatasetDetailPage.tsx b/src/pages/DatasetDetailPage.tsx
new file mode 100644
index 0000000..255541e
--- /dev/null
+++ b/src/pages/DatasetDetailPage.tsx
@@ -0,0 +1,1369 @@
+import PreviewModal from "../components/PreviewModal";
+import CloudDownloadIcon from "@mui/icons-material/CloudDownload";
+import DescriptionIcon from "@mui/icons-material/Description";
+import ExpandLess from "@mui/icons-material/ExpandLess";
+import ExpandMore from "@mui/icons-material/ExpandMore";
+import HomeIcon from "@mui/icons-material/Home";
+import {
+ Box,
+ Typography,
+ CircularProgress,
+ Backdrop,
+ Alert,
+ Button,
+ Card,
+ CardContent,
+ Collapse,
+} from "@mui/material";
+import { TextField } from "@mui/material";
+import LoadDatasetTabs from "components/DatasetDetailPage/LoadDatasetTabs";
+import theme, { Colors } from "design/theme";
+import { useAppDispatch } from "hooks/useAppDispatch";
+import { useAppSelector } from "hooks/useAppSelector";
+import React, { useEffect, useMemo, useState } from "react";
+import ReactJson from "react-json-view";
+import { useParams, useNavigate } from "react-router-dom";
+import { fetchDocumentDetails } from "redux/neurojson/neurojson.action";
+import { NeurojsonSelector } from "redux/neurojson/neurojson.selector";
+import RoutesEnum from "types/routes.enum";
+
+interface ExternalDataLink {
+ name: string;
+ size: string;
+ path: string;
+ url: string;
+ index: number;
+}
+
+interface InternalDataLink {
+ name: string;
+ data: any;
+ index: number;
+ arraySize?: number[];
+}
+
+const transformJsonForDisplay = (obj: any): any => {
+ if (typeof obj !== "object" || obj === null) return obj;
+
+ const transformed: any = Array.isArray(obj) ? [] : {};
+
+ for (const key in obj) {
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
+
+ const value = obj[key];
+
+ // Match README, CHANGES, or file extensions
+ const isLongTextKey = /^(README|CHANGES)$|\.md$|\.txt$|\.m$/i.test(key);
+
+ if (typeof value === "string" && isLongTextKey) {
+ transformed[key] = `${value}`;
+ } else if (typeof value === "object") {
+ transformed[key] = transformJsonForDisplay(value);
+ } else {
+ transformed[key] = value;
+ }
+ }
+
+ return transformed;
+};
+
+const formatAuthorsWithDOI = (
+ authors: string[] | string,
+ doi: string
+): JSX.Element => {
+ let authorText = "";
+
+ if (Array.isArray(authors)) {
+ if (authors.length === 1) {
+ authorText = authors[0];
+ } else if (authors.length === 2) {
+ authorText = authors.join(", ");
+ } else {
+ authorText = `${authors.slice(0, 2).join("; ")} et al.`;
+ }
+ } else {
+ authorText = authors;
+ }
+
+ let doiUrl = "";
+ if (doi) {
+ if (/^[0-9]/.test(doi)) {
+ doiUrl = `https://doi.org/${doi}`;
+ } else if (/^doi\./.test(doi)) {
+ doiUrl = `https://${doi}`;
+ } else if (/^doi:/.test(doi)) {
+ doiUrl = doi.replace(/^doi:/, "https://doi.org/");
+ } else {
+ doiUrl = doi;
+ }
+ }
+
+ return (
+ <>
+ {authorText}
+ {doiUrl && (
+
+ (e.currentTarget.style.textDecoration = "underline")
+ }
+ onMouseLeave={(e) => (e.currentTarget.style.textDecoration = "none")}
+ >
+ {doiUrl}
+
+ )}
+ >
+ );
+};
+
+const DatasetDetailPage: React.FC = () => {
+ const { dbName, docId } = useParams<{ dbName: string; docId: string }>();
+ const navigate = useNavigate();
+ const dispatch = useAppDispatch();
+ const {
+ selectedDocument: datasetDocument,
+ loading,
+ error,
+ } = useAppSelector(NeurojsonSelector);
+
+ const [externalLinks, setExternalLinks] = useState([]);
+ const [internalLinks, setInternalLinks] = useState([]);
+ const [isExpanded, setIsExpanded] = useState(false);
+ const [isInternalExpanded, setIsInternalExpanded] = useState(true);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [matches, setMatches] = useState([]);
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
+ const [downloadScript, setDownloadScript] = useState("");
+ const [downloadScriptSize, setDownloadScriptSize] = useState(0);
+ const [totalFileSize, setTotalFileSize] = useState(0);
+
+ const [previewIsInternal, setPreviewIsInternal] = useState(false);
+ const [isExternalExpanded, setIsExternalExpanded] = useState(true);
+ const [expandedPaths, setExpandedPaths] = useState([]);
+ const [originalTextMap, setOriginalTextMap] = useState<
+ Map
+ >(new Map());
+ const [jsonViewerKey, setJsonViewerKey] = useState(0);
+ const [jsonSize, setJsonSize] = useState(0);
+ const [transformedDataset, setTransformedDataset] = useState(null);
+ const [previewIndex, setPreviewIndex] = useState(0);
+
+ // add spinner
+ const [isPreviewLoading, setIsPreviewLoading] = useState(false);
+ const [readyPreviewData, setReadyPreviewData] = useState(null);
+
+ // const onPreviewReady = (decodedData: any) => {
+ // console.log("✅ Data is ready! Opening modal.");
+ // setReadyPreviewData(decodedData); // Store the final data
+ // setIsPreviewLoading(false); // Hide the spinner
+ // setPreviewOpen(true); // NOW open the modal
+ // };
+
+ // Dataset download button size calculation function
+ // const formatSize = (sizeInBytes: number): string => {
+ // if (sizeInBytes < 1024 * 1024) {
+ // return `${(sizeInBytes / 1024).toFixed(1)} KB`;
+ // }
+ // return `${(sizeInBytes / 1024 / 1024).toFixed(2)} MB`;
+ // };
+ const formatSize = (sizeInBytes: number): string => {
+ if (sizeInBytes < 1024) {
+ return `${sizeInBytes} Bytes`;
+ } else if (sizeInBytes < 1024 * 1024) {
+ return `${(sizeInBytes / 1024).toFixed(1)} KB`;
+ } else if (sizeInBytes < 1024 * 1024 * 1024) {
+ return `${(sizeInBytes / (1024 * 1024)).toFixed(2)} MB`;
+ } else if (sizeInBytes < 1024 * 1024 * 1024 * 1024) {
+ return `${(sizeInBytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
+ } else {
+ return `${(sizeInBytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`;
+ }
+ };
+
+ // Recursive function to find `_DataLink_`
+ const extractDataLinks = (obj: any, path: string): ExternalDataLink[] => {
+ const links: ExternalDataLink[] = [];
+
+ const traverse = (
+ node: any,
+ currentPath: string,
+ parentKey: string = ""
+ ) => {
+ if (typeof node === "object" && node !== null) {
+ for (const key in node) {
+ if (key === "_DataLink_" && typeof node[key] === "string") {
+ let correctedUrl = node[key].replace(/:\$.*$/, "");
+ const sizeMatch = node[key].match(/size=(\d+)/);
+ const size = sizeMatch
+ ? `${(parseInt(sizeMatch[1], 10) / 1024 / 1024).toFixed(2)} MB`
+ : "Unknown Size";
+
+ const parts = currentPath.split("/");
+ const subpath = parts.slice(-3).join("/");
+ const label = parentKey || "ExternalData";
+
+ links.push({
+ name: `${label} (${size}) [/${subpath}]`,
+ size,
+ path: currentPath, // keep full JSON path for file placement
+ url: correctedUrl,
+ index: links.length,
+ });
+ } else if (typeof node[key] === "object") {
+ const isMetaKey = key.startsWith("_");
+ const newLabel = !isMetaKey ? key : parentKey;
+ traverse(node[key], `${currentPath}/${key}`, newLabel);
+ }
+ }
+ }
+ };
+
+ traverse(obj, path);
+ // return links;
+ const seenUrls = new Set();
+ const uniqueLinks = links.filter((link) => {
+ if (seenUrls.has(link.url)) return false;
+ seenUrls.add(link.url);
+ return true;
+ });
+
+ return uniqueLinks;
+ };
+
+ const extractInternalData = (obj: any, path = ""): InternalDataLink[] => {
+ const internalLinks: InternalDataLink[] = [];
+
+ if (obj && typeof obj === "object") {
+ if (
+ obj.hasOwnProperty("MeshNode") &&
+ (obj.hasOwnProperty("MeshSurf") || obj.hasOwnProperty("MeshElem"))
+ ) {
+ if (
+ obj.MeshNode.hasOwnProperty("_ArrayZipData_") &&
+ typeof obj.MeshNode["_ArrayZipData_"] === "string"
+ ) {
+ internalLinks.push({
+ name: `JMesh`,
+ data: obj,
+ index: internalLinks.length, // maybe can be remove
+ arraySize: obj.MeshNode._ArraySize_,
+ });
+ }
+ } else if (obj.hasOwnProperty("NIFTIData")) {
+ if (
+ obj.NIFTIData.hasOwnProperty("_ArrayZipData_") &&
+ typeof obj.NIFTIData["_ArrayZipData_"] === "string"
+ ) {
+ internalLinks.push({
+ name: `JNIfTI`,
+ data: obj,
+ index: internalLinks.length, //maybe can be remove
+ arraySize: obj.NIFTIData._ArraySize_,
+ });
+ }
+ } else if (
+ obj.hasOwnProperty("_ArraySize_") &&
+ !path.match("_EnumValue_$")
+ ) {
+ if (
+ obj.hasOwnProperty("_ArrayZipData_") &&
+ typeof obj["_ArrayZipData_"] === "string"
+ ) {
+ internalLinks.push({
+ name: `JData`,
+ data: obj,
+ index: internalLinks.length, // maybe can be remove
+ arraySize: obj._ArraySize_,
+ });
+ }
+ } else {
+ Object.keys(obj).forEach((key) => {
+ if (typeof obj[key] === "object") {
+ internalLinks.push(
+ ...extractInternalData(
+ obj[key],
+ `${path}.${key.replace(/\./g, "\\.")}`
+ )
+ );
+ }
+ });
+ }
+ }
+
+ return internalLinks;
+ };
+
+ // const formatFileSize = (bytes: number): string => {
+ // if (bytes >= 1024 * 1024 * 1024) {
+ // return `${Math.floor(bytes / (1024 * 1024 * 1024))} GB`;
+ // } else if (bytes >= 1024 * 1024) {
+ // return `${Math.floor(bytes / (1024 * 1024))} MB`;
+ // } else {
+ // return `${Math.floor(bytes / 1024)} KB`;
+ // }
+ // };
+
+ useEffect(() => {
+ const fetchData = async () => {
+ if (dbName && docId) {
+ await dispatch(fetchDocumentDetails({ dbName, docId }));
+ // console.log("dbName", dbName);
+ // console.log("docId", docId);
+ }
+ };
+
+ fetchData();
+ }, [dbName, docId, dispatch]);
+
+ useEffect(() => {
+ if (datasetDocument) {
+ // ✅ Extract External Data & Assign `index`
+ console.log("datasetDocument", datasetDocument);
+ const links = extractDataLinks(datasetDocument, "").map(
+ (link, index) => ({
+ ...link,
+ index, // ✅ Assign index correctly
+ })
+ );
+
+ // ✅ Extract Internal Data & Assign `index`
+ const internalData = extractInternalData(datasetDocument).map(
+ (data, index) => ({
+ ...data,
+ index, // ✅ Assign index correctly
+ })
+ );
+
+ // console.log("🟢 Extracted external links:", links);
+ // console.log("🟢 Extracted internal data:", internalData);
+
+ setExternalLinks(links);
+ setInternalLinks(internalData);
+ const transformed = transformJsonForDisplay(datasetDocument);
+ setTransformedDataset(transformed);
+
+ // Calculate total file size from size= query param
+ let total = 0;
+ links.forEach((link) => {
+ const sizeMatch = link.url.match(/(?:[?&]size=)(\d+)/);
+ if (sizeMatch && sizeMatch[1]) {
+ total += parseInt(sizeMatch[1], 10);
+ }
+ });
+ setTotalFileSize(total);
+
+ let totalSize = 0;
+
+ // 1️⃣ Sum external link sizes (from URL like ...?size=12345678)
+ links.forEach((link) => {
+ const sizeMatch = link.url.match(/size=(\d+)/);
+ if (sizeMatch) {
+ totalSize += parseInt(sizeMatch[1], 10);
+ }
+ });
+
+ // 2️⃣ Estimate internal size from _ArraySize_ (assume Float32 = 4 bytes)
+ internalData.forEach((link) => {
+ if (link.arraySize && Array.isArray(link.arraySize)) {
+ const count = link.arraySize.reduce((acc, val) => acc * val, 1);
+ totalSize += count * 4;
+ }
+ });
+
+ // setTotalFileSize(totalSize);
+
+ // const minifiedBlob = new Blob([JSON.stringify(datasetDocument)], {
+ // type: "application/json",
+ // });
+ // setJsonSize(minifiedBlob.size);
+
+ const blob = new Blob([JSON.stringify(datasetDocument, null, 2)], {
+ type: "application/json",
+ });
+ setJsonSize(blob.size);
+
+ // // ✅ Construct download script dynamically
+ let script = `curl -L --create-dirs "https://neurojson.io:7777/${dbName}/${docId}" -o "${docId}.json"\n`;
+
+ links.forEach((link) => {
+ const url = link.url;
+ // console.log("url", url);
+ const match = url.match(/file=([^&]+)/);
+ // console.log("match", match);
+ // console.log("match[1]", match?.[1]);
+ // try {
+ // const decoded = match?.[1] ? decodeURIComponent(match[1]) : "N/A";
+ // console.log("decode", decoded);
+ // } catch (err) {
+ // console.warn("⚠️ Failed to decode match[1]:", match?.[1], err);
+ // }
+
+ // const filename = match
+ // ? decodeURIComponent(match[1])
+ // : `file-${link.index}`;
+
+ const filename = match
+ ? (() => {
+ try {
+ return decodeURIComponent(match[1]);
+ } catch {
+ return match[1]; // fallback if decode fails
+ }
+ })()
+ : `file-${link.index}`;
+ // console.log("filename", filename);
+
+ const outputPath = `$HOME/.neurojson/io/${dbName}/${docId}/${filename}`;
+
+ script += `curl -L --create-dirs "${url}" -o "${outputPath}"\n`;
+ });
+ setDownloadScript(script);
+ // ✅ Calculate and set script size
+ const scriptBlob = new Blob([script], { type: "text/plain" });
+ setDownloadScriptSize(scriptBlob.size);
+ }
+ }, [datasetDocument, docId]);
+
+ const [previewOpen, setPreviewOpen] = useState(false);
+ const [previewDataKey, setPreviewDataKey] = useState(null);
+
+ useEffect(() => {
+ highlightMatches(searchTerm);
+
+ // Cleanup to reset highlights when component re-renders or unmounts
+ return () => {
+ document.querySelectorAll(".highlighted").forEach((el) => {
+ const element = el as HTMLElement;
+ const text = element.textContent || "";
+ element.innerHTML = text;
+ element.classList.remove("highlighted");
+ });
+ };
+ }, [searchTerm, datasetDocument]);
+
+ useEffect(() => {
+ if (!transformedDataset) return;
+
+ const spans = document.querySelectorAll(".string-value");
+
+ spans.forEach((el) => {
+ if (el.textContent?.includes('')) {
+ // Inject as HTML so it renders code block correctly
+ el.innerHTML = el.textContent ?? "";
+ }
+ });
+ }, [transformedDataset]);
+
+ const handleDownloadDataset = () => {
+ if (!datasetDocument) return;
+ const jsonData = JSON.stringify(datasetDocument);
+ const blob = new Blob([jsonData], { type: "application/json" });
+ const link = document.createElement("a");
+ link.href = URL.createObjectURL(blob);
+ link.download = `${docId}.json`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ };
+
+ const handleDownloadScript = () => {
+ const blob = new Blob([downloadScript], { type: "text/plain" });
+ const link = document.createElement("a");
+ link.href = URL.createObjectURL(blob);
+ link.download = `${docId}.sh`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ };
+
+ const handlePreview = (
+ dataOrUrl: string | any,
+ idx: number,
+ isInternal: boolean = false
+ ) => {
+ console.log(
+ "🟢 Preview button clicked for:",
+ dataOrUrl,
+ "Index:",
+ idx,
+ "Is Internal:",
+ isInternal
+ );
+ // fix spinner
+ setIsPreviewLoading(true); // Show the spinner overlay
+ setPreviewIndex(idx);
+ setPreviewDataKey(dataOrUrl);
+ setPreviewIsInternal(isInternal);
+
+ // setPreviewOpen(false); // IMPORTANT: Keep modal closed for now
+
+ // This callback will be triggered by the legacy script when data is ready
+ // (window as any).__onPreviewReady = (decodedData: any) => {
+ // console.log("✅ Data is ready! Opening modal.");
+ // setReadyPreviewData(decodedData); // Store the final data for the modal
+ // setIsPreviewLoading(false); // Hide the spinner
+ // setPreviewOpen(true); // NOW it's time to open the modal
+ // };
+
+ const is2DPreviewCandidate = (obj: any): boolean => {
+ if (!obj || typeof obj !== "object") return false;
+ if (!obj._ArrayType_ || !obj._ArraySize_ || !obj._ArrayZipData_)
+ return false;
+ const dim = obj._ArraySize_;
+ return (
+ Array.isArray(dim) &&
+ (dim.length === 1 || dim.length === 2) &&
+ dim.every((v) => typeof v === "number" && v > 0)
+ );
+ };
+ // for add spinner ---- start
+ // When legacy preview is actually ready, turn off spinner & open modal
+ window.__onPreviewReady = () => {
+ setIsPreviewLoading(false);
+ // Only open modal for 3D data
+ if (!is2DPreviewCandidate(dataOrUrl)) {
+ setPreviewOpen(true);
+ }
+ delete window.__onPreviewReady;
+ };
+ // -----end
+
+ const extractFileName = (url: string): string => {
+ const match = url.match(/file=([^&]+)/);
+ // return match ? decodeURIComponent(match[1]) : url;
+ if (match) {
+ // Strip any trailing query parameters
+ const raw = decodeURIComponent(match[1]);
+ return raw.split("?")[0].split("&")[0];
+ }
+ // fallback: try to get last path part if no 'file=' param
+ try {
+ const u = new URL(url);
+ const parts = u.pathname.split("/");
+ return parts[parts.length - 1];
+ } catch {
+ return url;
+ }
+ };
+
+ const fileName =
+ typeof dataOrUrl === "string" ? extractFileName(dataOrUrl) : "";
+ console.log("🔍 Extracted fileName:", fileName);
+
+ const isPreviewableFile = (fileName: string): boolean => {
+ return /\.(nii\.gz|jdt|jdb|bmsh|jmsh|bnii)$/i.test(fileName);
+ };
+ console.log("🧪 isPreviewableFile:", isPreviewableFile(fileName));
+
+ // test for add spinner
+ // if (isInternal) {
+ // if (is2DPreviewCandidate(dataOrUrl)) {
+ // // inline 2D
+ // window.dopreview(dataOrUrl, idx, true);
+ // } else {
+ // // 3D
+ // window.previewdata(dataOrUrl, idx, true, []);
+ // }
+ // } else {
+ // // external
+ // window.previewdataurl(dataOrUrl, idx);
+ // }
+
+ // for test so command out the below
+ // setPreviewIndex(idx);
+ // setPreviewDataKey(dataOrUrl);
+ // setPreviewIsInternal(isInternal);
+ // setPreviewOpen(true);
+
+ if (isInternal) {
+ try {
+ if (!(window as any).intdata) {
+ (window as any).intdata = [];
+ }
+ if (!(window as any).intdata[idx]) {
+ (window as any).intdata[idx] = ["", "", null, `Internal ${idx}`];
+ }
+ (window as any).intdata[idx][2] = JSON.parse(JSON.stringify(dataOrUrl));
+
+ const is2D = is2DPreviewCandidate(dataOrUrl);
+
+ if (is2D) {
+ console.log("📊 2D data → rendering inline with dopreview()");
+ (window as any).dopreview(dataOrUrl, idx, true);
+ const panel = document.getElementById("chartpanel");
+ if (panel) panel.style.display = "block"; // 🔓 Show it!
+ setPreviewOpen(false); // ⛔ Don't open modal
+ // setPreviewLoading(false); // stop spinner
+ } else {
+ console.log("🎬 3D data → rendering in modal");
+ (window as any).previewdata(dataOrUrl, idx, true, []);
+ // add spinner
+ // setPreviewDataKey(dataOrUrl);
+ // setPreviewOpen(true);
+ // setPreviewIsInternal(true);
+ }
+ } catch (err) {
+ console.error("❌ Error in internal preview:", err);
+ // setPreviewLoading(false); // add spinner
+ }
+ } else {
+ // external
+ // if (/\.(nii\.gz|jdt|jdb|bmsh|jmsh|bnii)$/i.test(dataOrUrl)) {
+ const fileName =
+ typeof dataOrUrl === "string" ? extractFileName(dataOrUrl) : "";
+ if (isPreviewableFile(fileName)) {
+ (window as any).previewdataurl(dataOrUrl, idx);
+ const panel = document.getElementById("chartpanel");
+ if (panel) panel.style.display = "none"; // 🔒 Hide chart panel on 3D external
+ //add spinner
+ // setPreviewDataKey(dataOrUrl);
+ // setPreviewOpen(true);
+ // setPreviewIsInternal(false);
+ } else {
+ console.warn("⚠️ Unsupported file format for preview:", dataOrUrl);
+ // setPreviewLoading(false); // add spinner
+ }
+ }
+ };
+
+ // const handleClosePreview = () => {
+ // console.log("🛑 Closing preview modal.");
+ // setPreviewOpen(false);
+ // setPreviewDataKey(null);
+
+ // // Stop any Three.js rendering when modal closes
+ // if (typeof (window as any).update === "function") {
+ // cancelAnimationFrame((window as any).reqid);
+ // }
+
+ // const panel = document.getElementById("chartpanel");
+ // if (panel) panel.style.display = "none"; // 🔒 Hide 2D chart if modal closes
+ // };
+ const handleClosePreview = () => {
+ console.log("🛑 Closing preview modal.");
+ setPreviewOpen(false);
+ setPreviewDataKey(null);
+
+ // Cancel animation frame loop
+ if (typeof window.reqid !== "undefined") {
+ cancelAnimationFrame(window.reqid);
+ window.reqid = undefined;
+ }
+
+ // Stop 2D chart if any
+ const panel = document.getElementById("chartpanel");
+ if (panel) panel.style.display = "none";
+
+ // Remove canvas children
+ // const canvasDiv = document.getElementById("canvas");
+ // if (canvasDiv) {
+ // while (canvasDiv.firstChild) {
+ // canvasDiv.removeChild(canvasDiv.firstChild);
+ // }
+ // }
+
+ // Reset Three.js global refs
+ window.scene = undefined;
+ window.camera = undefined;
+ window.renderer = undefined;
+ };
+
+ const handleSearch = (e: React.ChangeEvent) => {
+ setSearchTerm(e.target.value);
+ setHighlightedIndex(-1);
+ highlightMatches(e.target.value);
+ };
+
+ const highlightMatches = (keyword: string) => {
+ const spans = document.querySelectorAll(
+ ".react-json-view span.string-value, .react-json-view span.object-key"
+ );
+
+ // Clean up all existing highlights
+ spans.forEach((el) => {
+ const element = el as HTMLElement;
+ if (originalTextMap.has(element)) {
+ element.innerHTML = originalTextMap.get(element)!; // Restore original HTML
+ element.classList.remove("highlighted");
+ }
+ });
+
+ // Clear old state
+ setMatches([]);
+ setHighlightedIndex(-1);
+ setExpandedPaths([]);
+ setOriginalTextMap(new Map());
+
+ if (!keyword.trim() || keyword.length < 3) return;
+
+ const regex = new RegExp(`(${keyword})`, "gi");
+ const matchedElements: HTMLElement[] = [];
+ const matchedPaths: Set = new Set();
+ const newOriginalMap = new Map();
+
+ spans.forEach((el) => {
+ const element = el as HTMLElement;
+ const original = element.innerHTML;
+ const text = element.textContent || "";
+
+ if (text.toLowerCase().includes(keyword.toLowerCase())) {
+ newOriginalMap.set(element, original); // Store original HTML
+ const highlighted = text.replace(
+ regex,
+ `$1`
+ );
+ element.innerHTML = highlighted;
+ matchedElements.push(element);
+
+ const parent = element.closest(".variable-row");
+ const path = parent?.getAttribute("data-path");
+ if (path) matchedPaths.add(path);
+ }
+ });
+
+ // Update state
+ setOriginalTextMap(newOriginalMap);
+ setMatches(matchedElements);
+ setExpandedPaths(Array.from(matchedPaths));
+ };
+
+ const findNext = () => {
+ if (matches.length === 0) return;
+
+ setHighlightedIndex((prevIndex) => {
+ const nextIndex = (prevIndex + 1) % matches.length;
+
+ matches.forEach((match) => {
+ match
+ .querySelector("mark")
+ ?.setAttribute("style", "background: yellow; color: black;");
+ });
+
+ const current = matches[nextIndex];
+ current.scrollIntoView({ behavior: "smooth", block: "center" });
+
+ current
+ .querySelector("mark")
+ ?.setAttribute("style", "background: orange; color: black;");
+
+ return nextIndex;
+ });
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ {error}
+
+
+ );
+ }
+ console.log("datasetDocument", datasetDocument);
+ const onekey = datasetDocument
+ ? datasetDocument.hasOwnProperty("README")
+ ? "README"
+ : datasetDocument.hasOwnProperty("dataset_description.json")
+ ? "dataset_description.json"
+ : "_id"
+ : "_id";
+
+ return (
+ <>
+ {/* 🔧 Inline CSS for string formatting */}
+
+
+
+
+
+ {/* ✅ Dataset Title (From dataset_description.json) */}
+
+ {datasetDocument?.["dataset_description.json"]?.Name ??
+ `Dataset: ${docId}`}
+
+
+ {/* ✅ Dataset Author (If Exists) */}
+ {datasetDocument?.["dataset_description.json"]?.Authors && (
+
+ {Array.isArray(
+ datasetDocument["dataset_description.json"].Authors
+ )
+ ? datasetDocument["dataset_description.json"].Authors.join(", ")
+ : datasetDocument["dataset_description.json"].Authors}
+
+ )}
+
+ {/* ✅ Breadcrumb Navigation (🏠 Home → Database → Dataset) */}
+
+ {/* 🏠 Home Icon Button */}
+
+
+
+ »
+
+
+ {/* Database Name (Clickable) */}
+
+
+
+ »
+
+
+ {/* Dataset Name (_id field) */}
+
+ {docId}
+
+
+
+
+ }
+ onClick={handleDownloadDataset}
+ sx={{
+ backgroundColor: Colors.purple,
+ color: Colors.lightGray,
+ "&:hover": { backgroundColor: Colors.secondaryPurple },
+ }}
+ >
+ {/* Download Dataset (1 Mb) */}
+ {/* Download Dataset ({(jsonSize / 1024).toFixed(0)} MB) */}
+ Download Matadata ({formatSize(jsonSize)})
+
+
+ }
+ onClick={handleDownloadScript}
+ sx={{
+ backgroundColor: Colors.purple,
+ color: Colors.lightGray,
+ "&:hover": { backgroundColor: Colors.secondaryPurple },
+ }}
+ >
+ {/* Script to Download All Files ({downloadScript.length} Bytes) */}
+ Script to Download All Files ({formatSize(downloadScriptSize)})
+ {/* (links: {externalLinks.length}) */}
+ {externalLinks.length > 0 &&
+ ` (links: ${externalLinks.length}, total: ${formatSize(
+ totalFileSize
+ )})`}
+
+
+
+
+
+
+
+
+
+
+
+ {/* ✅ JSON Viewer (left panel) */}
+
+ = 3 ? false : 1} // 🔍 Expand during search
+ style={{ fontSize: "14px", fontFamily: "monospace" }}
+ />
+
+
+ {/* ✅ Data panels (right panel) */}
+
+
+ {/* ✅ Collapsible header */}
+ setIsInternalExpanded(!isInternalExpanded)}
+ >
+
+ Internal Data ({internalLinks.length} objects)
+
+ {isInternalExpanded ? : }
+
+
+
+ {/* ✅ Scrollable area */}
+
+ {internalLinks.length > 0 ? (
+ internalLinks.map((link, index) => (
+
+
+ {link.name}{" "}
+ {link.arraySize
+ ? `[${link.arraySize.join("x")}]`
+ : ""}
+
+
+
+ ))
+ ) : (
+
+ No internal data found.
+
+ )}
+
+
+
+
+ {/* ✅ Header with toggle */}
+ setIsExternalExpanded(!isExternalExpanded)}
+ >
+
+ External Data ({externalLinks.length} links)
+
+ {isExternalExpanded ? : }
+
+
+
+ {/* Scrollable card container */}
+
+ {externalLinks.length > 0 ? (
+ externalLinks.map((link, index) => {
+ const match = link.url.match(/file=([^&]+)/);
+ const fileName = match ? match[1] : "";
+ const isPreviewable =
+ /\.(nii(\.gz)?|bnii|jdt|jdb|jmsh|bmsh)$/i.test(
+ fileName
+ );
+
+ return (
+
+
+ {link.name}
+
+
+
+ {isPreviewable && (
+
+ )}
+
+
+ );
+ })
+ ) : (
+
+ No external links found.
+
+ )}
+
+
+
+
+
+ {/* ✅ ADD FLASHCARDS COMPONENT HERE ✅ */}
+
+ {/* */}
+
+
+ {/*
+
+ {/* Global spinner while loading (before modal mounts) */}
+
+
+
+
+ {/* Preview Modal Component - Add Here */}
+
+
+ >
+ );
+};
+
+export default DatasetDetailPage;
diff --git a/src/pages/DatasetPage.tsx b/src/pages/DatasetPage.tsx
new file mode 100644
index 0000000..978eb6d
--- /dev/null
+++ b/src/pages/DatasetPage.tsx
@@ -0,0 +1,476 @@
+import {
+ fetchDbInfo,
+ loadPaginatedData,
+} from "../redux/neurojson/neurojson.action";
+import { Row } from "../redux/neurojson/types/neurojson.interface";
+import {
+ Box,
+ Typography,
+ CircularProgress,
+ Alert,
+ Card,
+ CardContent,
+ Grid,
+ Link,
+ Chip,
+ Stack,
+ Button,
+ Select,
+ MenuItem,
+ FormControl,
+ InputLabel,
+} from "@mui/material";
+import { Colors } from "design/theme";
+import { useAppDispatch } from "hooks/useAppDispatch";
+import { useAppSelector } from "hooks/useAppSelector";
+import React, { useEffect, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import RoutesEnum from "types/routes.enum";
+
+const DatasetPage: React.FC = () => {
+ const navigate = useNavigate();
+ const { dbName } = useParams<{ dbName: string }>();
+ const dispatch = useAppDispatch();
+ const { loading, error, data, limit, hasMore } = useAppSelector(
+ (state: { neurojson: any }) => state.neurojson
+ );
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize, setPageSize] = useState(10);
+ const totalPages = Math.ceil(limit / pageSize);
+ const [searchQuery, setSearchQuery] = useState("");
+
+ useEffect(() => {
+ if (dbName) {
+ dispatch(fetchDbInfo(dbName.toLowerCase()));
+ dispatch(
+ loadPaginatedData({
+ dbName: dbName.toLowerCase(),
+ offset: (currentPage - 1) * pageSize,
+ limit: pageSize,
+ })
+ );
+ }
+ }, [dbName, dispatch, currentPage, pageSize]);
+
+ const handlePageChange = (page: number) => {
+ if (!dbName || loading) return;
+
+ setCurrentPage(page);
+ dispatch(
+ loadPaginatedData({
+ dbName: dbName.toLowerCase(),
+ offset: (page - 1) * pageSize,
+ limit: pageSize,
+ })
+ );
+ };
+
+ const handlePageSizeChange = (event: any) => {
+ setPageSize(event.target.value);
+ setCurrentPage(1); // Reset offset when changing page size
+ };
+
+ const getVisiblePageNumbers = () => {
+ const visiblePages: (number | string)[] = [];
+ const maxVisible = 6;
+
+ if (totalPages <= maxVisible + 2) {
+ for (let i = 1; i <= totalPages; i++) visiblePages.push(i);
+ } else {
+ const start = Math.max(2, currentPage - 2);
+ const end = Math.min(totalPages - 1, currentPage + 2);
+
+ visiblePages.push(1);
+ if (start > 2) visiblePages.push("...");
+
+ for (let i = start; i <= end; i++) visiblePages.push(i);
+ if (end < totalPages - 1) visiblePages.push("...");
+
+ visiblePages.push(totalPages);
+ }
+
+ return visiblePages;
+ };
+
+ const handlePrevNextPage = (direction: "prev" | "next") => {
+ if (direction === "prev" && currentPage > 1) {
+ handlePageChange(currentPage - 1);
+ } else if (direction === "next" && currentPage < totalPages) {
+ handlePageChange(currentPage + 1);
+ }
+ };
+
+ const filteredData = data.filter((doc: Row) =>
+ (doc.value.name || "").toLowerCase().includes(searchQuery.toLowerCase())
+ );
+
+ return (
+
+
+ {/* Left: Title */}
+
+ Database: {dbName || "N/A"}
+
+
+ {/* Right: Total + Dropdown + Pagination */}
+
+ {/* Left: Total datasets */}
+
+ Total datasets: {limit}
+
+
+ {/* Search in page input */}
+
+
+ Search in page:
+
+ setSearchQuery(e.target.value)}
+ style={{
+ padding: "6px 10px",
+ borderRadius: "4px",
+ border: `2px solid ${Colors.primary.main}`,
+ fontSize: "0.95rem",
+ minWidth: "200px",
+ }}
+ />
+
+
+ {/* Right: Label + Select in one line */}
+
+
+ Dataset per page:
+
+ {/* Dataset per page dropdown */}
+
+
+
+
+ {/* Pagination buttons */}
+ {!loading && (
+
+
+
+ {getVisiblePageNumbers().map((item, idx) =>
+ item === "..." ? (
+
+ ...
+
+ ) : (
+
+ )
+ )}
+
+
+
+ )}
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {loading && (
+
+ )}
+
+ {!loading && !error && data.length > 0 && (
+
+ {filteredData.map((doc: any, index: number) => {
+ const datasetIndex = (currentPage - 1) * pageSize + index + 1;
+ return (
+
+
+ {/* Dataset index number on top-right corner */}
+
+ {datasetIndex}
+
+
+
+
+
+ ID: {doc.id}
+
+
+
+
+ {doc.value.subj && (
+
+ )}
+ {doc.value.modality &&
+ doc.value.modality.map((mod: string) => (
+
+ ))}
+
+
+
+ Summary:{" "}
+ {doc.value.readme || "No description available"}
+
+
+
+ Authors:{" "}
+ {Array.isArray(doc.value.info?.Authors)
+ ? doc.value.info.Authors.join(", ")
+ : doc.value.info?.Authors || "Unknown"}
+
+
+
+
+ Size:{" "}
+ {doc.value.length
+ ? `${(doc.value.length / 1024 / 1024).toFixed(
+ 2
+ )} MB`
+ : "Unknown"}
+
+
+ {doc.value.info?.DatasetDOI && (
+
+
+
+ )}
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+ {!loading && !error && data.length === 0 && (
+
+ No database information available.
+
+ )}
+
+ );
+};
+
+export default DatasetPage;
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index 6e0982c..ff922d8 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -1,21 +1,121 @@
-import { Container } from "@mui/material";
+import { Box, Container } from "@mui/material";
+import Section1 from "components/HomePageComponents/Section1";
+import Section2 from "components/HomePageComponents/Section2";
+import Section3 from "components/HomePageComponents/Section3";
+import Section4 from "components/HomePageComponents/Section4";
+import NodeInfoPanel from "components/NodeInfoPanel";
import { useAppDispatch } from "hooks/useAppDispatch";
import { useAppSelector } from "hooks/useAppSelector";
-import NeuroJsonGraph from "modules/universe/NeuroJsonGraph";
-import React, { useEffect } from "react";
+import { NodeObject } from "modules/universe/NeuroJsonGraph";
+import React, {
+ useEffect,
+ useState,
+ useCallback,
+ useMemo,
+ useRef,
+} from "react";
import { fetchRegistry } from "redux/neurojson/neurojson.action";
import { NeurojsonSelector } from "redux/neurojson/neurojson.selector";
const Home: React.FC = () => {
- const dispatch = useAppDispatch();
- const { registry } = useAppSelector(NeurojsonSelector);
-
- useEffect(() => {
- dispatch(fetchRegistry());
- }, [dispatch]);
- return (
- {registry && }
- );
+ const dispatch = useAppDispatch();
+ const section2Ref = useRef(null);
+ const section3Ref = useRef(null);
+ const section4Ref = useRef(null);
+ const { registry, loading } = useAppSelector(NeurojsonSelector);
+
+ // State for selected node and panel visibility
+ const [selectedNode, setSelectedNode] = useState(null);
+ const [panelOpen, setPanelOpen] = useState(false);
+ const [filterKeyword, setFilterKeyword] = useState(""); // State for filter input
+ const [selectedModalities, setSelectedModalities] = useState([]);
+
+ useEffect(() => {
+ dispatch(fetchRegistry());
+ }, [dispatch]);
+
+ // Handle node click: Set selected node and open panel
+ const handleNodeClick = useCallback((node: NodeObject) => {
+ setSelectedNode(node);
+ setPanelOpen(true);
+ }, []);
+
+ // filter logic
+ const filteredRegistry = useMemo(() => {
+ return registry
+ ? registry.filter((node) => {
+ const matchKeyword = node.name
+ .toLowerCase()
+ .includes(filterKeyword.toLowerCase());
+ const matchModalities =
+ selectedModalities.length === 0 ||
+ selectedModalities.some((modality) =>
+ Array.isArray(node.datatype)
+ ? node.datatype.includes(modality)
+ : node.datatype === modality
+ );
+ return matchKeyword && matchModalities;
+ })
+ : [];
+ }, [registry, filterKeyword, selectedModalities]);
+
+ return (
+
+ {/* section 1 */}
+
+
+ section2Ref.current?.scrollIntoView({ behavior: "smooth" })
+ }
+ />
+
+
+ {/* section 2 */}
+
+
+ section3Ref.current?.scrollIntoView({ behavior: "smooth" })
+ }
+ />
+
+
+ {/* section 3 */}
+
+
+ section4Ref.current?.scrollIntoView({ behavior: "smooth" })
+ }
+ />
+
+
+ {/* section 4*/}
+
+
+
+
+ setPanelOpen(false)}
+ nodeData={selectedNode}
+ />
+
+ );
};
export default Home;
diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx
new file mode 100644
index 0000000..005fb10
--- /dev/null
+++ b/src/pages/SearchPage.tsx
@@ -0,0 +1,634 @@
+import { generateSchemaWithDatabaseEnum } from "../utils/SearchPageFunctions/searchformSchema";
+import ArrowCircleRightIcon from "@mui/icons-material/ArrowCircleRight";
+import {
+ Typography,
+ Container,
+ Box,
+ Button,
+ CircularProgress,
+ Pagination,
+ Chip,
+ Drawer,
+} from "@mui/material";
+import { useTheme } from "@mui/material/styles";
+import useMediaQuery from "@mui/material/useMediaQuery";
+import Form from "@rjsf/mui";
+import validator from "@rjsf/validator-ajv8";
+import DatasetCard from "components/SearchPage/DatasetCard";
+import SubjectCard from "components/SearchPage/SubjectCard";
+import { Colors } from "design/theme";
+import { useAppDispatch } from "hooks/useAppDispatch";
+import { useAppSelector } from "hooks/useAppSelector";
+import pako from "pako";
+import React from "react";
+import { useState, useEffect, useMemo } from "react";
+import {
+ fetchMetadataSearchResults,
+ fetchRegistry,
+} from "redux/neurojson/neurojson.action";
+import { RootState } from "redux/store";
+import { generateUiSchema } from "utils/SearchPageFunctions/generateUiSchema";
+import { modalityValueToEnumLabel } from "utils/SearchPageFunctions/modalityLabels";
+
+const SearchPage: React.FC = () => {
+ const dispatch = useAppDispatch();
+ const [hasSearched, setHasSearched] = useState(false);
+ const searchResults = useAppSelector(
+ (state: RootState) => state.neurojson.searchResults
+ );
+ const registry = useAppSelector(
+ (state: RootState) => state.neurojson.registry
+ );
+ const loading = useAppSelector((state: RootState) => state.neurojson.loading);
+
+ const [formData, setFormData] = useState>({});
+ const [showSubjectFilters, setShowSubjectFilters] = useState(false);
+ const [results, setResults] = useState<
+ any[] | { status: string; msg: string }
+ >([]);
+ const [skip, setSkip] = useState(0);
+ const [page, setPage] = useState(1);
+ const [queryLink, setQueryLink] = useState("");
+ const [appliedFilters, setAppliedFilters] = useState>({});
+ const [showMobileFilters, setShowMobileFilters] = useState(false);
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
+
+ // to show the applied chips on the top of results
+ const activeFilters = Object.entries(appliedFilters).filter(
+ ([key, value]) =>
+ key !== "skip" &&
+ key !== "limit" &&
+ value !== undefined &&
+ value !== null &&
+ value !== "" &&
+ value !== "any"
+ );
+
+ // parse query from url on page load
+ useEffect(() => {
+ const hash = window.location.hash;
+ if (hash.startsWith("#query=")) {
+ const encoded = hash.replace("#query=", "");
+ try {
+ const decoded = pako.inflate(
+ Uint8Array.from(atob(encoded), (c) => c.charCodeAt(0)),
+ { to: "string" }
+ );
+ const parsed = JSON.parse(decoded);
+ setFormData(parsed);
+ setAppliedFilters(parsed);
+ const requestData = { ...parsed, skip: 0, limit: 50 };
+ setSkip(0);
+ setHasSearched(true);
+ dispatch(fetchMetadataSearchResults(requestData)).then((res: any) => {
+ if (res.payload) {
+ setResults(res.payload);
+ }
+ });
+ } catch (e) {
+ console.error("Failed to parse query from URL", e);
+ }
+ }
+ }, [dispatch]);
+
+ // generate a direct link to the query
+ const updateQueryLink = (queryData: Record) => {
+ const deflated = pako.deflate(JSON.stringify(queryData));
+ const encoded = btoa(String.fromCharCode(...deflated));
+ const link = `${window.location.origin}${window.location.pathname}#query=${encoded}`;
+ setQueryLink(link);
+ };
+
+ // setting pagination
+ const itemsPerPage = 10;
+
+ const handlePageChange = (
+ event: React.ChangeEvent,
+ value: number
+ ) => {
+ setPage(value);
+ };
+
+ const paginatedResults = Array.isArray(results)
+ ? results.slice((page - 1) * itemsPerPage, page * itemsPerPage)
+ : [];
+
+ // form UI
+ const uiSchema = useMemo(
+ () => generateUiSchema(formData, showSubjectFilters),
+ [formData, showSubjectFilters]
+ );
+
+ // Create the "Subject-level Filters" button as a custom field
+ const customFields = {
+ subjectFiltersToggle: () => (
+
+
+
+ ),
+ };
+
+ // determine the results are subject-level or dataset-level
+ let isDataset: boolean | null = null;
+
+ if (Array.isArray(searchResults) && searchResults.length > 0) {
+ try {
+ const parsed = JSON.parse(searchResults[0].json);
+ isDataset = parsed?.value?.subj && Array.isArray(parsed.value.subj);
+ } catch {
+ isDataset = null;
+ }
+ }
+
+ // get the database list from registry
+ useEffect(() => {
+ dispatch(fetchRegistry());
+ }, [dispatch]);
+
+ // dynamically add database enum to schema
+ const schema = useMemo(() => {
+ const dbList = registry?.length
+ ? [...registry.map((item: any) => item.id), "any"]
+ : ["any"];
+ return generateSchemaWithDatabaseEnum(dbList);
+ }, [registry]);
+
+ // submit function
+ const handleSubmit = ({ formData }: any) => {
+ const requestData = { ...formData, skip: 0 };
+ setFormData(requestData);
+ setSkip(0);
+ setAppliedFilters(requestData); // for chips on the top of results
+
+ dispatch(fetchMetadataSearchResults(requestData)).then((res: any) => {
+ setResults(res.payload);
+ });
+ setHasSearched(true);
+ setPage(1);
+ updateQueryLink(formData);
+ };
+
+ // reset function
+ const handleReset = () => {
+ setFormData({});
+ setResults([]);
+ setHasSearched(false);
+ setSkip(0);
+ dispatch(fetchMetadataSearchResults({}));
+ setPage(1);
+ setQueryLink("");
+ setAppliedFilters({});
+ };
+
+ // load more function
+ const handleLoadMore = () => {
+ const newSkip = skip + 50;
+ const requestData = { ...formData, skip: newSkip };
+ setSkip(newSkip);
+ dispatch(fetchMetadataSearchResults(requestData)).then((res: any) => {
+ if (Array.isArray(res.payload)) {
+ setResults((prev) =>
+ Array.isArray(prev) ? [...prev, ...res.payload] : res.payload
+ );
+ }
+ });
+ };
+
+ // handle chips click function
+ const handleChipClick = (key: string, value: string) => {
+ const updatedFormData: Record = {
+ ...formData,
+ [key]:
+ key === "modality" ? modalityValueToEnumLabel[value] || value : value,
+ skip: 0,
+ };
+ setFormData(updatedFormData);
+ setSkip(0);
+ setPage(1);
+ updateQueryLink(updatedFormData);
+ dispatch(fetchMetadataSearchResults(updatedFormData)).then((res: any) => {
+ setResults(res.payload);
+ });
+ setHasSearched(true);
+ setAppliedFilters(updatedFormData); // for chips on top of results
+ };
+
+ // form rendering
+ const renderFilterForm = () => (
+ <>
+ {queryLink && activeFilters.length > 0 && (
+
+
+
+ Direct Link to This Query
+
+
+
+
+ )}
+
+
+
+
+