Skip to content
25 changes: 18 additions & 7 deletions src/components/DatasetDetailPage/MetaDataPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ParticipantsPreview from "./ParticipantsPreview";
import ArrowCircleRightIcon from "@mui/icons-material/ArrowCircleRight";
import {
Box,
Expand Down Expand Up @@ -190,13 +191,23 @@ const MetaDataPanel: React.FC<Props> = ({
})()}
</Typography>
</Box>
<Box>
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
Subjects
</Typography>
<Typography sx={{ color: "text.secondary" }}>
{dbViewInfo?.rows?.[0]?.value?.subj?.length ?? "N/A"}
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "row",
gap: 2,
alignItems: "flex-start",
}}
>
<Box>
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
Subjects
</Typography>
<Typography sx={{ color: "text.secondary" }}>
{dbViewInfo?.rows?.[0]?.value?.subj?.length ?? "N/A"}
</Typography>
</Box>
<ParticipantsPreview datasetDocument={datasetDocument} />
</Box>
<Box>
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
Expand Down
97 changes: 97 additions & 0 deletions src/components/DatasetDetailPage/ParticipantsPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { makeParticipantsTable } from "../../utils/DatasetDetailPageFunctions/participants";
import {
Box,
Button,
Dialog,
DialogContent,
DialogTitle,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
} from "@mui/material";
import { Colors } from "design/theme";
import React, { useMemo, useState } from "react";

type Props = {
datasetDocument: any;
};

const ParticipantsPreview: React.FC<Props> = ({ datasetDocument }) => {
const [open, setOpen] = useState(false);

const table = useMemo(() => {
const part = datasetDocument?.["participants.tsv"];
return makeParticipantsTable(part);
}, [datasetDocument]);

if (!table) return null; // No participants.tsv found

return (
<>
<Box>
<Button
variant="outlined"
size="small"
onClick={() => setOpen(true)}
sx={{
color: Colors.purple,
borderColor: Colors.purple,
"&:hover": {
color: Colors.secondaryPurple,
transform: "scale(1.01)",
borderColor: Colors.purple,
},
}}
>
Participants Table Preview
</Button>
</Box>

<Dialog
open={open}
onClose={() => setOpen(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>participants.tsv</DialogTitle>
<DialogContent dividers>
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
{table.columns.map((col) => (
<TableCell
key={col}
sx={{
fontWeight: "bold",
backgroundColor: Colors.darkPurple,
color: Colors.white,
}}
>
{col.replace(/_/g, " ")}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{table.rows.map((row, rowIdx) => (
<TableRow key={rowIdx}>
{table.columns.map((col) => (
<TableCell key={col}>{row[col]}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</DialogContent>
</Dialog>
</>
);
};

export default ParticipantsPreview;
2 changes: 1 addition & 1 deletion src/components/SearchPage/DatabaseCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const DatabaseCard: React.FC<Props> = ({
}) => {
const dispatch = useAppDispatch();
const dbInfo = useAppSelector((state: RootState) => state.neurojson.dbInfo);
console.log("dbInfo", dbInfo);
// console.log("dbInfo", dbInfo);
useEffect(() => {
if (dbId) {
dispatch(fetchDbInfo(dbId.toLowerCase()));
Expand Down
120 changes: 110 additions & 10 deletions src/components/SearchPage/DatasetCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Typography, Card, CardContent, Stack, Chip } from "@mui/material";
import { Colors } from "design/theme";
import React from "react";
import { useMemo } from "react";
import { Link } from "react-router-dom";
import RoutesEnum from "types/routes.enum";

Expand All @@ -17,14 +18,73 @@ interface DatasetCardProps {
info?: {
Authors?: string[];
DatasetDOI?: string;
[k: string]: any;
};
[k: string]: any;
};
};
index: number;
onChipClick: (key: string, value: string) => void;
keyword?: string; // for keyword highlight
}

/** ---------- utility helpers ---------- **/
const normalize = (s: string) =>
s
?.replace(/[\u2018\u2019\u2032]/g, "'") // curly → straight
?.replace(/[\u201C\u201D\u2033]/g, '"') ?? // curly → straight
"";

const containsKeyword = (text?: string, kw?: string) => {
if (!text || !kw) return false;
const t = normalize(text).toLowerCase();
const k = normalize(kw).toLowerCase();
return t.includes(k);
};

/** Find a short snippet in secondary fields if not already visible */
function findMatchSnippet(
v: any,
kw?: string
): { label: string; html: string } | null {
if (!kw) return null;

// Which fields to scan (can add/remove fields here)
const CANDIDATE_FIELDS: Array<[string, (v: any) => string | undefined]> = [
["Acknowledgements", (v) => v?.info?.Acknowledgements],
[
"Funding",
(v) =>
Array.isArray(v?.info?.Funding)
? v.info.Funding.join(" ")
: v?.info?.Funding,
],
["ReferencesAndLinks", (v) => v?.info?.ReferencesAndLinks],
];

const k = normalize(kw).toLowerCase();

for (const [label, getter] of CANDIDATE_FIELDS) {
const raw = getter(v); // v = parsedJson.value
if (!raw) continue;
const text = normalize(String(raw));
const i = text.toLowerCase().indexOf(k); // k is the lowercase version of keyword
if (i >= 0) {
const start = Math.max(0, i - 40);
const end = Math.min(text.length, i + k.length + 40);
const before = text.slice(start, i);
const hit = text.slice(i, i + k.length);
const after = text.slice(i + k.length, end);
const html = `${
start > 0 ? "…" : ""
}${before}<mark>${hit}</mark>${after}${end < text.length ? "…" : ""}`;
return { label, html };
}
}
return null;
}
/** ---------- end of helpers ---------- **/

const DatasetCard: React.FC<DatasetCardProps> = ({
dbname,
dsname,
Expand All @@ -40,7 +100,29 @@ const DatasetCard: React.FC<DatasetCardProps> = ({
const rawDOI = info?.DatasetDOI?.replace(/^doi:/, "");
const doiLink = rawDOI ? `https://doi.org/${rawDOI}` : null;

// keyword hightlight functional component
// precompute what’s visible & whether it already contains the keyword
const authorsJoined = Array.isArray(info?.Authors)
? info!.Authors.join(", ")
: typeof info?.Authors === "string"
? info!.Authors
: "";

const visibleHasKeyword = useMemo(
() =>
containsKeyword(name, keyword) ||
containsKeyword(readme, keyword) ||
containsKeyword(authorsJoined, keyword),
[name, readme, authorsJoined, keyword]
);

// If not visible, produce a one-line snippet from other fields (for non-visible fields)
const snippet = useMemo(
() =>
!visibleHasKeyword ? findMatchSnippet(parsedJson.value, keyword) : null,
[parsedJson.value, keyword, visibleHasKeyword]
);

// keyword hightlight functional component (only for visible fields)
const highlightKeyword = (text: string, keyword?: string) => {
if (!keyword || !text?.toLowerCase().includes(keyword.toLowerCase())) {
return text;
Expand Down Expand Up @@ -99,7 +181,10 @@ const DatasetCard: React.FC<DatasetCardProps> = ({
{highlightKeyword(name || "Untitled Dataset", keyword)}
</Typography>
<Typography>
Database: {dbname} &nbsp;&nbsp;|&nbsp;&nbsp; Dataset: {dsname}
{/* Database: {dbname} &nbsp;&nbsp;|&nbsp;&nbsp; Dataset: {dsname} */}
<strong>Database:</strong> {highlightKeyword(dbname, keyword)}
{" "}&nbsp;&nbsp;|&nbsp;&nbsp;{" "}
<strong>Dataset:</strong> {highlightKeyword(dsname, keyword)}
</Typography>

<Stack spacing={2} margin={1}>
Expand Down Expand Up @@ -168,20 +253,35 @@ const DatasetCard: React.FC<DatasetCardProps> = ({
{info?.Authors && (
<Typography variant="body2" mt={1}>
<strong>Authors:</strong>{" "}
{highlightKeyword(
Array.isArray(info.Authors)
? info.Authors.join(", ")
: typeof info.Authors === "string"
? info.Authors
: "N/A",
keyword
)}
{highlightKeyword(authorsJoined || "N/A", keyword)}
</Typography>
)}
</Typography>
)}
</Stack>

{/* show why it matched if not visible in main fields */}
{snippet && (
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
<Chip
label={`Matched in ${snippet.label}`}
size="small"
sx={{
height: 22,
backgroundColor: "#f9f9ff",
color: Colors.darkPurple,
border: `1px solid ${Colors.lightGray}`,
}}
/>
<Typography
variant="body2"
sx={{ mt: 0.5 }}
// safe: snippet is derived from our own strings with <mark> only
dangerouslySetInnerHTML={{ __html: snippet.html }}
/>
</Stack>
)}

<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
{doiLink && (
<Stack mt={1}>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/DatasetDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ const DatasetDetailPage: React.FC = () => {
useEffect(() => {
if (datasetDocument) {
// ✅ Extract External Data & Assign `index`
console.log("datasetDocument", datasetDocument);
// console.log("datasetDocument", datasetDocument);
const links = extractDataLinks(datasetDocument, "").map(
(link, index) => ({
...link,
Expand Down
Loading