Skip to content

Commit 43229bf

Browse files
authored
Merge pull request #9 from Health-Informatics-UoN/improve_notes
Improve notes
2 parents e2845da + e9cc61d commit 43229bf

14 files changed

Lines changed: 11115 additions & 1941 deletions

File tree

app/api/concepts/route.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { NextResponse } from "next/server";
22
import { getConcepts } from "@/lib/services/conceptsService";
33

4-
54
export async function GET(req: Request) {
6-
const { searchParams } = new URL(req.url);
7-
const domain = searchParams.get("domain") || undefined;
8-
9-
const concepts = await getConcepts(domain);
5+
try {
6+
const { searchParams } = new URL(req.url);
7+
const domain = searchParams.get("domain") || "All";
8+
9+
const concepts = await getConcepts(domain);
10+
11+
return NextResponse.json(concepts);
12+
} catch (error) {
13+
console.error("API ERROR:", error);
1014

11-
return NextResponse.json(concepts);
12-
}
15+
return NextResponse.json(
16+
{ error: "Failed to fetch concepts" },
17+
{ status: 500 }
18+
);
19+
}
20+
}

app/globals.css

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,78 @@
11
@import "tailwindcss";
22

3-
:root {
4-
--background: #ffffff;
5-
--foreground: #171717;
3+
@theme inline {
4+
--color-background: var(--background);
5+
--color-foreground: var(--foreground);
6+
--color-card: var(--card);
7+
--color-card-foreground: var(--card-foreground);
8+
--color-popover: var(--popover);
9+
--color-popover-foreground: var(--popover-foreground);
10+
--color-primary: var(--primary);
11+
--color-primary-foreground: var(--primary-foreground);
12+
--color-secondary: var(--secondary);
13+
--color-secondary-foreground: var(--secondary-foreground);
14+
--color-muted: var(--muted);
15+
--color-muted-foreground: var(--muted-foreground);
16+
--color-accent: var(--accent);
17+
--color-accent-foreground: var(--accent-foreground);
18+
--color-destructive: var(--destructive);
19+
--color-border: var(--border);
20+
--color-input: var(--input);
21+
--color-ring: var(--ring);
22+
--color-chart-1: var(--chart-1);
23+
--color-chart-2: var(--chart-2);
24+
--color-chart-3: var(--chart-3);
25+
--color-chart-4: var(--chart-4);
26+
--color-chart-5: var(--chart-5);
27+
--color-sidebar: var(--sidebar);
28+
--color-sidebar-foreground: var(--sidebar-foreground);
29+
--color-sidebar-primary: var(--sidebar-primary);
30+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
31+
--color-sidebar-accent: var(--sidebar-accent);
32+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
33+
--color-sidebar-border: var(--sidebar-border);
34+
--color-sidebar-ring: var(--sidebar-ring);
35+
--radius-sm: calc(var(--radius) * 0.6);
36+
--radius-md: calc(var(--radius) * 0.8);
37+
--radius-lg: var(--radius);
38+
--radius-xl: calc(var(--radius) * 1.4);
39+
--radius-2xl: calc(var(--radius) * 1.8);
40+
--radius-3xl: calc(var(--radius) * 2.2);
41+
--radius-4xl: calc(var(--radius) * 2.6);
42+
}
43+
:root {
44+
--radius: 0.625rem;
45+
--background: oklch(1 0 0);
46+
--foreground: oklch(0.145 0 0);
47+
--card: oklch(1 0 0);
48+
--card-foreground: oklch(0.145 0 0);
49+
--popover: oklch(1 0 0);
50+
--popover-foreground: oklch(0.145 0 0);
51+
--primary: oklch(0.205 0 0);
52+
--primary-foreground: oklch(0.985 0 0);
53+
--secondary: oklch(0.97 0 0);
54+
--secondary-foreground: oklch(0.205 0 0);
55+
--muted: oklch(0.97 0 0);
56+
--muted-foreground: oklch(0.556 0 0);
57+
--accent: oklch(0.97 0 0);
58+
--accent-foreground: oklch(0.205 0 0);
59+
--destructive: oklch(0.577 0.245 27.325);
60+
--border: oklch(0.922 0 0);
61+
--input: oklch(0.922 0 0);
62+
--ring: oklch(0.708 0 0);
63+
--chart-1: oklch(0.646 0.222 41.116);
64+
--chart-2: oklch(0.6 0.118 184.704);
65+
--chart-3: oklch(0.398 0.07 227.392);
66+
--chart-4: oklch(0.828 0.189 84.429);
67+
--chart-5: oklch(0.769 0.188 70.08);
68+
--sidebar: oklch(0.985 0 0);
69+
--sidebar-foreground: oklch(0.145 0 0);
70+
--sidebar-primary: oklch(0.205 0 0);
71+
--sidebar-primary-foreground: oklch(0.985 0 0);
72+
--sidebar-accent: oklch(0.97 0 0);
73+
--sidebar-accent-foreground: oklch(0.205 0 0);
74+
--sidebar-border: oklch(0.922 0 0);
75+
--sidebar-ring: oklch(0.708 0 0);
676
}
777

878
@theme inline {
@@ -24,3 +94,4 @@ body {
2494
color: var(--foreground);
2595
font-family: Arial, Helvetica, sans-serif;
2696
}
97+

app/page.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,18 @@ export default function Home() {
1616
useEffect(() => {
1717
fetch(`/api/concepts?domain=${domain}`)
1818
.then((res) => res.json())
19-
.then(setConcepts);
19+
.then((data) => {
20+
if (Array.isArray(data)) {
21+
setConcepts(data);
22+
} else {
23+
console.error("Concepts is not an array:", data);
24+
setConcepts([]);
25+
}
26+
})
27+
.catch((err) => {
28+
console.error("Fetch error:", err);
29+
setConcepts([]);
30+
});
2031
}, [domain]);
2132

2233
const loadNotes = async (conceptId: string) => {

components.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema.json",
3+
"style": "radix-nova",
4+
"rsc": true,
5+
"tsx": true,
6+
"tailwind": {
7+
"config": "",
8+
"css": "app/globals.css",
9+
"baseColor": "neutral",
10+
"cssVariables": true,
11+
"prefix": ""
12+
},
13+
"iconLibrary": "lucide",
14+
"rtl": false,
15+
"aliases": {
16+
"components": "@/components",
17+
"utils": "@/lib/utils",
18+
"ui": "@/components/ui",
19+
"lib": "@/lib",
20+
"hooks": "@/hooks"
21+
},
22+
"menuColor": "default",
23+
"menuAccent": "subtle",
24+
"registries": {}
25+
}

components/NoteCard.tsx

Lines changed: 64 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,69 @@
1-
import { Note } from '@/types/OmopTables';
2-
import { useState } from 'react';
3-
1+
import { Note } from "@/types/OmopTables";
2+
import { useState } from "react";
3+
import { Button } from "@/components/ui/button";
4+
import {
5+
Card,
6+
CardContent,
7+
CardHeader,
8+
CardFooter,
9+
CardTitle,
10+
} from "@/components/ui/card";
11+
import { Badge } from "@/components/ui/badge";
12+
import { ArrowRightIcon } from "lucide-react";
413
export default function NoteCard({ note }: { note: Note }) {
5-
const pmcid = note.note_source_value?.match(/PMC(\d+)/)?.[1];
6-
const [article, setArticle] = useState<any>(null);
7-
8-
const loadArticle = async () => {
9-
if (!pmcid) return;
10-
const res = await fetch(`/api/pmc?pmcid=${pmcid}`);
11-
const data = await res.json();
12-
setArticle(data);
13-
if (data.articleUrl) {
14-
window.open(data.articleUrl, '_blank');
15-
} else {
16-
alert('No article found');
17-
}
18-
19-
};
20-
21-
return (
22-
<div className="border border-gray-200 rounded-xl p-4 bg-white shadow-sm hover:shadow-md transition">
23-
<div className="text-sm text-gray-500">
24-
Note ID: {note.note_id}
25-
</div>
26-
<div className="text-sm text-gray-500">
27-
PMC ID: {pmcid || 'N/A'}
28-
</div>
29-
<div className="text-sm">
30-
Date: {note.note_date}
31-
</div>
32-
33-
{pmcid && (
34-
35-
<button
36-
onClick={loadArticle}
37-
className="mt-2 bg-blue-500 text-white px-3 py-1 rounded"
38-
>
39-
Go to Article
40-
</button>
14+
const pmcid = note.note_source_value?.match(/PMC(\d+)/)?.[1];
15+
const [article, setArticle] = useState<any>(null);
16+
const uniqueConcepts = Array.from(
17+
new Map((note.concepts || []).map((c: any) => [c.concept_id, c])).values(),
18+
);
19+
const loadArticle = async () => {
20+
if (!pmcid) return;
21+
const res = await fetch(`/api/pmc?pmcid=${pmcid}`);
22+
const data = await res.json();
23+
setArticle(data);
24+
if (data.articleUrl) {
25+
window.open(data.articleUrl, "_blank");
26+
} else {
27+
alert("No article found");
28+
}
29+
};
4130

42-
)}
31+
return (
32+
<Card>
33+
<CardHeader>
34+
<CardTitle>PMC ID: {pmcid || "N/A"}</CardTitle>
35+
</CardHeader>
36+
<CardContent className="flex flex-wrap items-center gap-2 md:flex-row">
4337

38+
<p>
39+
{pmcid && (
40+
<Button variant="outline" size="sm" onClick={loadArticle}>
41+
Go to Article
42+
<ArrowRightIcon />
43+
</Button>
44+
)}
45+
</p>
46+
</CardContent>
4447

48+
<CardFooter>
49+
<div className="flex flex-wrap gap-2">
50+
{uniqueConcepts.map((c: any) => (
51+
<span
52+
key={c.concept_id}
53+
className={`px-2 py-1 text-xs rounded-md
54+
${c.domain === "Condition" ? "bg-sky-100" : ""}
55+
${c.domain === "Drug" ? "bg-emerald-100" : ""}
56+
`}
57+
>
58+
<Badge variant="ghost">
59+
{c.concept_name}
60+
</Badge>
61+
</span>
62+
))}
4563
</div>
46-
);
47-
}
64+
</CardFooter>
65+
66+
</Card>
67+
68+
);
69+
}

components/ui/badge.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as React from "react"
2+
import { cva, type VariantProps } from "class-variance-authority"
3+
import { Slot } from "radix-ui"
4+
5+
import { cn } from "@/lib/utils"
6+
7+
const badgeVariants = cva(
8+
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
9+
{
10+
variants: {
11+
variant: {
12+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13+
secondary:
14+
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
15+
destructive:
16+
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
17+
outline:
18+
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
19+
ghost:
20+
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
21+
link: "text-primary underline-offset-4 hover:underline",
22+
},
23+
},
24+
defaultVariants: {
25+
variant: "default",
26+
},
27+
}
28+
)
29+
30+
function Badge({
31+
className,
32+
variant = "default",
33+
asChild = false,
34+
...props
35+
}: React.ComponentProps<"span"> &
36+
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
37+
const Comp = asChild ? Slot.Root : "span"
38+
39+
return (
40+
<Comp
41+
data-slot="badge"
42+
data-variant={variant}
43+
className={cn(badgeVariants({ variant }), className)}
44+
{...props}
45+
/>
46+
)
47+
}
48+
49+
export { Badge, badgeVariants }

components/ui/button.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import * as React from "react"
2+
import { cva, type VariantProps } from "class-variance-authority"
3+
import { Slot } from "radix-ui"
4+
5+
import { cn } from "@/lib/utils"
6+
7+
const buttonVariants = cva(
8+
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9+
{
10+
variants: {
11+
variant: {
12+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13+
outline:
14+
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
15+
secondary:
16+
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
17+
ghost:
18+
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
19+
destructive:
20+
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
21+
link: "text-primary underline-offset-4 hover:underline",
22+
},
23+
size: {
24+
default:
25+
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
26+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
27+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
28+
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
29+
icon: "size-8",
30+
"icon-xs":
31+
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
32+
"icon-sm":
33+
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
34+
"icon-lg": "size-9",
35+
},
36+
},
37+
defaultVariants: {
38+
variant: "default",
39+
size: "default",
40+
},
41+
}
42+
)
43+
44+
function Button({
45+
className,
46+
variant = "default",
47+
size = "default",
48+
asChild = false,
49+
...props
50+
}: React.ComponentProps<"button"> &
51+
VariantProps<typeof buttonVariants> & {
52+
asChild?: boolean
53+
}) {
54+
const Comp = asChild ? Slot.Root : "button"
55+
56+
return (
57+
<Comp
58+
data-slot="button"
59+
data-variant={variant}
60+
data-size={size}
61+
className={cn(buttonVariants({ variant, size, className }))}
62+
{...props}
63+
/>
64+
)
65+
}
66+
67+
export { Button, buttonVariants }

0 commit comments

Comments
 (0)