Skip to content

Commit 3ce7a49

Browse files
committed
add hover over concepts and support groups
1 parent 1e31530 commit 3ce7a49

13 files changed

Lines changed: 405 additions & 138 deletions

components/ConceptHover.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"use client";
2+
3+
import {
4+
HoverCard,
5+
HoverCardContent,
6+
HoverCardTrigger,
7+
} from "@/components/ui/hover-card";
8+
import { InfoCircledIcon } from "@radix-ui/react-icons";
9+
10+
export function ConceptHover({
11+
conceptName,
12+
relatedConcepts,
13+
}: {
14+
conceptName: string;
15+
relatedConcepts?: { conceptId: string; conceptName: string }[];
16+
}) {
17+
return (
18+
<HoverCard>
19+
<HoverCardTrigger asChild>
20+
<span className="flex items-center text-2xl cursor-pointer">
21+
{conceptName}
22+
<InfoCircledIcon className="ml-1" />
23+
</span>
24+
</HoverCardTrigger>
25+
26+
<HoverCardContent className="w-80">
27+
<div className="space-y-2">
28+
<div className="font-semibold">Related Concepts:</div>
29+
30+
{relatedConcepts?.length ? (
31+
<div className="text-sm max-h-40 overflow-auto space-y-1">
32+
{relatedConcepts.map((c) => (
33+
<div key={c.conceptId} className="flex flex-col">
34+
<span>{c.conceptName}</span>
35+
<span className="text-xs text-muted-foreground">
36+
{c.conceptId}
37+
</span>
38+
</div>
39+
))}
40+
</div>
41+
) : (
42+
<div className="text-sm text-muted-foreground">
43+
No related concepts.
44+
</div>
45+
)}
46+
</div>
47+
</HoverCardContent>
48+
</HoverCard>
49+
);
50+
}

components/ConceptList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ export default function ConceptList({ concepts }: { concepts: Concept[] }) {
99
const router = useRouter();
1010
const pathname = usePathname();
1111
const searchParams = useSearchParams();
12-
const selectedConcepts = searchParams.getAll("conceptId");
12+
const selectedConcepts = searchParams.getAll("conceptId").sort();
1313

1414
const onSelect = (conceptId: string) => {
15-
const currentParams = searchParams.getAll("conceptId");
15+
const currentParams = searchParams.getAll("conceptId").sort();
1616

1717
let nextConceptIds: string[];
1818

components/DomainSelect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default function DomainSelect({ domain }: { domain: string }) {
2222
}
2323

2424
// Keep selected concepts
25-
const conceptIds = searchParams.getAll("conceptId");
25+
const conceptIds = searchParams.getAll("conceptId").sort();
2626
conceptIds.forEach((id) => params.append("conceptId", id));
2727

2828
// Reset page number

components/NoteCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
CollapsibleTrigger,
1515
} from "@/components/ui/collapsible";
1616
import { Badge } from "@/components/ui/badge";
17-
import { ArrowRightIcon, ChevronDown } from "lucide-react";
17+
import { ChevronDownIcon } from "@radix-ui/react-icons";
1818
import Link from 'next/link'
1919
export default function NoteCard({
2020
note,
@@ -47,7 +47,7 @@ export default function NoteCard({
4747
<CollapsibleTrigger asChild>
4848
<Button variant="outline" className="group w-full bg-transparent">
4949
Description
50-
<ChevronDown className="ml-auto group-data-[state=open]:rotate-180" />
50+
<ChevronDownIcon className="ml-auto group-data-[state=open]:rotate-180" />
5151
</Button>
5252
</CollapsibleTrigger>
5353
<CollapsibleContent className="flex flex-col items-start gap-2 p-2.5 pt-0 text-sm">

components/NotesList.tsx

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,25 @@ import { Note } from "@/types/OmopTables";
33
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
44
import { getMultiplePmcArticles } from "@/lib/services/pmcService";
55
import { Badge } from "@/components/ui/badge";
6-
6+
import { ConceptHover } from "./ConceptHover";
77
export default async function NotesList({
8+
relatedConcepts,
89
notes,
9-
conceptIds,
10-
conceptName,
10+
conceptDetails,
1111
}: {
12+
relatedConcepts: Array<{
13+
inputConceptId: string;
14+
groupConcepts: Array<{
15+
conceptId: string;
16+
conceptName: string;
17+
}>;
18+
}>;
1219
notes: Note[];
13-
conceptIds: Array<string> | null;
14-
conceptName: string | null;
20+
conceptDetails: Array<{
21+
conceptId: string;
22+
conceptName: string;
23+
domain: string;
24+
}>;
1525
}) {
1626
// Sort the PMC IDs for caching
1727
const pmcids = Array.from(
@@ -21,30 +31,40 @@ export default async function NotesList({
2131
.filter((id): id is string => Boolean(id)),
2232
),
2333
).sort();
24-
2534
if (pmcids.length === 0) return;
2635
const articles =
2736
pmcids.length > 0 ? await getMultiplePmcArticles(pmcids) : {};
2837

2938
return (
3039
<Card className="col-span-2">
3140
<CardHeader>
32-
<CardTitle className="text-2xl">
33-
{conceptIds && conceptName
34-
? `Case Reports for ${conceptName}`
35-
: "No term selected"}
41+
<CardTitle className="flex flex-wrap gap-2 items-center">
42+
<span className="text-2xl">Case Reports for: </span>
43+
{conceptDetails.map((c) => {
44+
const related = relatedConcepts.find(
45+
(r) => r.inputConceptId === c.conceptId,
46+
);
47+
return (
48+
<ConceptHover
49+
key={c.conceptId}
50+
conceptName={c.conceptName}
51+
relatedConcepts={related?.groupConcepts}
52+
/>
53+
);
54+
})}
3655
</CardTitle>
3756
</CardHeader>
3857

3958
<CardContent>
40-
{!conceptIds ? (
59+
{!conceptDetails ? (
4160
<div className="text-muted-foreground text-center py-10">
4261
Select a concept from the sidebar to view the associated Case
4362
Reports.
4463
</div>
4564
) : notes.length === 0 ? (
4665
<div className="text-muted-foreground text-center py-10">
47-
No notes found for {conceptName}
66+
No notes found for
67+
{conceptDetails.map((c) => c.conceptName).join(", ")}.
4868
</div>
4969
) : (
5070
<div className="grid grid-cols-1 gap-4">

components/NotesPagination.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ export default function NotesPagination({
3030
params.set("domain", domain);
3131
}
3232
// Get conceptIds and add to URL
33-
const conceptIds = searchParams.getAll("conceptId");
33+
const conceptIds = searchParams.getAll("conceptId").sort();
3434
conceptIds.forEach((id) => params.append("conceptId", id));
35-
35+
3636
// Add the page number
3737
params.set("page", String(newPage));
3838

components/NotesSection.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,20 @@ export default async function NotesSection({
1717
);
1818
}
1919

20-
const { conceptDetails, notes, total } = await getNotesForConcept(
20+
const { conceptDetails, relatedConcepts, notes, total } = await getNotesForConcept(
2121
conceptIds,
2222
page,
2323
);
2424

2525
const pageSize = 10;
2626
const totalPages = Math.ceil(total / pageSize);
27-
2827
return (
2928
<div className="p-4 space-y-4">
3029
<div className="text-sm text-muted-foreground">{total} articles</div>
3130
<NotesList
31+
relatedConcepts={relatedConcepts}
3232
notes={notes}
33-
conceptIds={conceptIds}
34-
conceptName={conceptDetails.map((c) => c.conceptName).join(", ")}
33+
conceptDetails={conceptDetails}
3534
/>
3635
<NotesPagination page={page} totalPages={totalPages} />
3736
</div>

components/ui/hover-card.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import { HoverCard as HoverCardPrimitive } from "radix-ui"
5+
6+
import { cn } from "@/lib/utils"
7+
8+
function HoverCard({
9+
...props
10+
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
11+
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
12+
}
13+
14+
function HoverCardTrigger({
15+
...props
16+
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
17+
return (
18+
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
19+
)
20+
}
21+
22+
function HoverCardContent({
23+
className,
24+
align = "center",
25+
sideOffset = 4,
26+
...props
27+
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
28+
return (
29+
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
30+
<HoverCardPrimitive.Content
31+
data-slot="hover-card-content"
32+
align={align}
33+
sideOffset={sideOffset}
34+
className={cn(
35+
"z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
36+
className
37+
)}
38+
{...props}
39+
/>
40+
</HoverCardPrimitive.Portal>
41+
)
42+
}
43+
44+
export { HoverCard, HoverCardTrigger, HoverCardContent }

0 commit comments

Comments
 (0)