Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f356866
Merge pull request #76 from NeuroJSON/staging
elainefan331 Jul 24, 2025
dd513cf
Refactor StatisticsBanner with reusable StatItem component for cleane…
elainefan331 Jul 25, 2025
0001f5f
fix: make the nav items responsive to avoid two-line layout on screen…
elainefan331 Jul 25, 2025
5f754f6
feat: add tooptip to v1 for legacy site info
elainefan331 Jul 25, 2025
6c23312
css: update about page videos layout; refs #66
elainefan331 Jul 26, 2025
ff2cd95
feat: enable smooth scroll to tutorial videos when clicking icon butt…
elainefan331 Jul 27, 2025
219cf4a
feat: add download video to the about page; closes #66
elainefan331 Aug 4, 2025
d9847ab
fix: resolve issue with fNIRS data display to ensure correct rendering
elainefan331 Aug 12, 2025
c3ed4b6
fix: handle cached fNIRS/time-series data in preview to prevent stuck…
elainefan331 Aug 12, 2025
0704c19
fix: ensures is2DPreviewCandidate always relies on the preview.js output
elainefan331 Aug 14, 2025
0479086
refactor: clean up redundant logic in dataset detail page
elainefan331 Aug 14, 2025
94f0c02
feat: update section3 img to 4 clickable images
elainefan331 Aug 15, 2025
40da71b
feat: add src links for section3 images
elainefan331 Aug 15, 2025
2d2fc39
fix: update the img path of section3
elainefan331 Aug 15, 2025
fc9a0ec
feat: add ScrollToTop component to reset scroll on route change
elainefan331 Aug 18, 2025
af01303
feat: update the atlas img in section3
elainefan331 Aug 18, 2025
7fd733e
Merge pull request #78 from NeuroJSON/dev-fan
elainefan331 Aug 18, 2025
dccd1e0
feat: display AISummary section in dataset detail header
elainefan331 Aug 20, 2025
aecb924
feat: use database logo as card bg on database page
elainefan331 Aug 20, 2025
c55f31b
feat: split subjects json into folder style tree(test)
elainefan331 Aug 21, 2025
c6b37b8
feat: add AI summary title in dataset page
elainefan331 Aug 21, 2025
7206110
feat: display database logo as Avatar instead of card background
elainefan331 Aug 21, 2025
ae58f53
Merge pull request #79 from NeuroJSON/dev-fan
elainefan331 Aug 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/img/section3/atlas.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/section3/fnirs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/section3/mesh.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/section3/mri.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
67 changes: 67 additions & 0 deletions src/components/DatasetDetailPage/FileTree/FileTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import FileTreeRow from "./FileTreeRow";
import type { TreeNode } from "./types";
import FolderIcon from "@mui/icons-material/Folder";
import { Box, Typography } from "@mui/material";
import React from "react";

type Props = {
title: string;
tree: TreeNode[];
filesCount: number;
totalBytes: number;
onPreview: (url: string, index: number) => void;
};

const formatSize = (n: number) => {
if (n < 1024) return `${n} B`;
if (n < 1024 ** 2) return `${(n / 1024).toFixed(1)} KB`;
if (n < 1024 ** 3) return `${(n / 1024 ** 2).toFixed(2)} MB`;
if (n < 1024 ** 4) return `${(n / 1024 ** 3).toFixed(2)} GB`;
return `${(n / 1024 ** 4).toFixed(2)} TB`;
};

const FileTree: React.FC<Props> = ({
title,
tree,
filesCount,
totalBytes,
onPreview,
}) => (
<Box
sx={{
backgroundColor: "#fff",
borderRadius: 2,
border: "1px solid #e0e0e0",
height: "100%",
display: "flex",
flexDirection: "column",
minHeight: 0,
}}
>
<Box
sx={{
px: 2,
py: 1.5,
borderBottom: "1px solid #eee",
display: "flex",
alignItems: "center",
gap: 1,
flexShrink: 0,
}}
>
<FolderIcon />
<Typography sx={{ fontWeight: 700, flex: 1 }}>{title}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}>
Files: {filesCount} &nbsp; Size: {formatSize(totalBytes)}
</Typography>
</Box>

<Box sx={{ flex: 1, minHeight: 0, overflowY: "auto", py: 0.5 }}>
{tree.map((n) => (
<FileTreeRow key={n.path} node={n} level={0} onPreview={onPreview} />
))}
</Box>
</Box>
);

export default FileTree;
130 changes: 130 additions & 0 deletions src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import type { TreeNode } from "./types";
import { formatLeafValue, isPreviewable } from "./utils";
import DownloadIcon from "@mui/icons-material/Download";
import ExpandLess from "@mui/icons-material/ExpandLess";
import ExpandMore from "@mui/icons-material/ExpandMore";
import FolderIcon from "@mui/icons-material/Folder";
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
import VisibilityIcon from "@mui/icons-material/Visibility";
import { Box, Button, Collapse, Typography } from "@mui/material";
import React from "react";

type Props = {
node: TreeNode;
level: number;
onPreview: (url: string, index: number) => void;
};

const FileTreeRow: React.FC<Props> = ({ node, level, onPreview }) => {
const [open, setOpen] = React.useState(false);

if (node.kind === "folder") {
return (
<>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
py: 0.5,
px: 1,
cursor: "pointer",
"&:hover": { backgroundColor: "rgba(0,0,0,0.04)" },
}}
onClick={() => setOpen((o) => !o)}
>
<Box sx={{ pl: level * 1.25 }}>
<FolderIcon fontSize="small" />
</Box>
<Typography sx={{ fontWeight: 600, flex: 1 }}>{node.name}</Typography>
{open ? <ExpandLess /> : <ExpandMore />}
</Box>

<Collapse in={open} timeout="auto" unmountOnExit>
{node.children.map((child) => (
<FileTreeRow
key={child.path}
node={child}
level={level + 1}
onPreview={onPreview}
/>
))}
</Collapse>
</>
);
}

return (
<Box
sx={{ display: "flex", alignItems: "flex-start", gap: 1, py: 0.5, px: 1 }}
>
<Box sx={{ pl: level * 1.25, pt: "2px" }}>
<InsertDriveFileIcon fontSize="small" />
</Box>

<Box sx={{ flex: 1, minWidth: 0, overflow: "hidden" }}>
<Typography
title={node.name}
sx={{
fontWeight: 500,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{node.name}
</Typography>

{!node.link && node.value !== undefined && (
<Typography
title={
node.name === "_ArrayZipData_"
? "[compressed data]"
: typeof node.value === "string"
? node.value
: JSON.stringify(node.value)
}
sx={{
fontFamily: "monospace",
fontSize: "0.85rem",
color: "text.secondary",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
mt: 0.25,
}}
>
{node.name === "_ArrayZipData_"
? "[compressed data]"
: formatLeafValue(node.value)}
</Typography>
)}
</Box>

{node.link?.url && (
<Box sx={{ display: "flex", gap: 1, flexShrink: 0 }}>
<Button
size="small"
variant="text"
onClick={() => window.open(node.link!.url, "_blank")}
startIcon={<DownloadIcon fontSize="small" />}
>
Download
</Button>
{isPreviewable(node.link.url) && (
<Button
size="small"
variant="text"
startIcon={<VisibilityIcon fontSize="small" />}
onClick={() => onPreview(node.link!.url, node.link!.index)}
>
Preview
</Button>
)}
</Box>
)}
</Box>
);
};

export default FileTreeRow;
6 changes: 6 additions & 0 deletions src/components/DatasetDetailPage/FileTree/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type LinkMeta = { url: string; index: number };

// this value can be one of these types
export type TreeNode =
| { kind: "folder"; name: string; path: string; children: TreeNode[] }
| { kind: "file"; name: string; path: string; value?: any; link?: LinkMeta };
76 changes: 76 additions & 0 deletions src/components/DatasetDetailPage/FileTree/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { LinkMeta, TreeNode } from "./types";

export const isPreviewable = (url: string) =>
/\.(nii(\.gz)?|bnii|jdt|jdb|jmsh|bmsh)$/i.test(
(url.match(/file=([^&]+)/)?.[1] ?? url).toLowerCase()
);

export const formatLeafValue = (v: any): string => {
if (v === null) return "null";
const t = typeof v;
if (t === "number" || t === "boolean") return String(v);
if (t === "string") return v.length > 120 ? v.slice(0, 120) + "…" : v;
if (Array.isArray(v)) {
const n = v.length;
const head = v
.slice(0, 5)
.map((x) => (typeof x === "number" ? x : JSON.stringify(x)));
return n <= 5
? `[${head.join(", ")}]`
: `[${head.join(", ")}, …] (${n} items)`;
}
return ""; // if it is object, return as a folder
};

// ignore meta keys
export const shouldSkipKey = (key: string) =>
key === "_id" || key === "_rev" || key.startsWith(".");

// build path -> {url, index} lookup, built from extractDataLinks function
// if external link objects have {path, url, index}, build a Map for the tree
export const makeLinkMap = <
T extends { path: string; url: string; index: number }
>(
links: T[]
): Map<string, LinkMeta> => {
const m = new Map<string, LinkMeta>();
links.forEach((l) => m.set(l.path, { url: l.url, index: l.index }));
return m;
};

// Recursively convert the dataset JSON to a file-tree
export const buildTreeFromDoc = (
doc: any,
linkMap: Map<string, LinkMeta>,
curPath = ""
): TreeNode[] => {
if (!doc || typeof doc !== "object") return [];
const out: TreeNode[] = [];

Object.keys(doc).forEach((key) => {
if (shouldSkipKey(key)) return;

const val = doc[key];
const path = `${curPath}/${key}`;
const link = linkMap.get(path);

if (link) {
out.push({ kind: "file", name: key, path, link });
return;
}

if (val && typeof val === "object" && !Array.isArray(val)) {
out.push({
kind: "folder",
name: key,
path,
children: buildTreeFromDoc(val, linkMap, path),
});
return;
}

out.push({ kind: "file", name: key, path, value: val });
});

return out;
};
Loading
Loading