diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 1398d10..c1be751 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-query-devtools": "^5.74.6",
+ "dompurify": "^3.2.5",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.5.1",
@@ -1881,6 +1882,13 @@
"@types/react": "^19.0.0"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.31.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.0.tgz",
@@ -2414,6 +2422,15 @@
"node": ">=0.10"
}
},
+ "node_modules/dompurify": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.5.tgz",
+ "integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/dotenv": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 8dafe5a..6303ab7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,6 +12,7 @@
"dependencies": {
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-query-devtools": "^5.74.6",
+ "dompurify": "^3.2.5",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.5.1",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index c8b0d78..beee352 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,6 +1,3 @@
-import { useQueries } from "@tanstack/react-query";
-
-import { getArticleIds, getToc } from "./lib/api";
import { Language } from "./lib/types";
import ArticleSelector from "./components/ArticleSelector/ArticleSelector";
@@ -12,45 +9,29 @@ import useUIStore from "./store/uiStore";
import styles from "./App.module.css";
import CelexSelector from "./components/CelexSelector/CelexSelector";
+import { useTOC } from "./hooks/toc";
function App() {
const { numPanels, addPanel } = useUIStore();
const { celexId, articleId } = useNavState();
-
- const results = useQueries({
- queries: [
- {
- queryKey: ["articleIds", celexId],
- queryFn: () => getArticleIds(celexId!),
- enabled: !!celexId,
- },
- {
- queryKey: ["toc", celexId],
- queryFn: () => getToc(celexId!, Language.ENG),
- enabled: !!celexId,
- },
- ],
- });
-
- const isPending = results.some((result) => result.isPending);
- const error = results.find((result) => result.isError);
+ const { data: toc, isPending, error } = useTOC();
if (isPending) {
return
Loading...
;
}
if (error) {
- return Error: {error.error?.message}
;
+ return Error: {error.message}
;
}
return (
-
+
{Array.from({ length: numPanels }, (_, index) => (
{
+ if (c.type === "division") {
+ return renderDivision(c);
+ } else {
+ return (
+
+ );
+ }
+ });
+
+ if (div.level === 0) {
+ const title = `${div.title}: ${div.subtitle}`;
+ return (
+ // For top-level divisions, we can use optgroup
+
+ );
+ } else {
+ // HTML does not support nested optgroups, so we need to flatten the structure
+ return <>{contents}>;
+ }
+ }
+
return (
<>
- {articleId && articleId > 1 && (
-
- )}
- {articleId && articleId < articleIds[articleIds.length - 1] && (
-
- )}
>
);
}
diff --git a/frontend/src/components/Panel/Panel.tsx b/frontend/src/components/Panel/Panel.tsx
index da673f1..d302a57 100644
--- a/frontend/src/components/Panel/Panel.tsx
+++ b/frontend/src/components/Panel/Panel.tsx
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query";
+import DOMPurify from "dompurify";
import { useEffect, useRef, useState } from "react";
import { getArticle } from "../../lib/api";
@@ -84,7 +85,7 @@ function Panel({ celexId, language, articleId }: PanelProps) {
);
diff --git a/frontend/src/components/TOC/TOC.tsx b/frontend/src/components/TOC/TOC.tsx
index f835e0d..f0fa777 100644
--- a/frontend/src/components/TOC/TOC.tsx
+++ b/frontend/src/components/TOC/TOC.tsx
@@ -3,10 +3,8 @@ import { Division } from "../../lib/types";
import useNavState from "../../store/navStore";
import styles from "./TOC.module.css";
-type TOC = Division[];
-
type TOCProps = {
- toc: TOC;
+ toc: Division[];
};
function TOC({ toc }: TOCProps) {
diff --git a/frontend/src/hooks/toc.ts b/frontend/src/hooks/toc.ts
new file mode 100644
index 0000000..5b2cafc
--- /dev/null
+++ b/frontend/src/hooks/toc.ts
@@ -0,0 +1,14 @@
+import { useQuery } from "@tanstack/react-query";
+import { getToc } from "../lib/api";
+import { Language } from "../lib/types";
+import useNavState from "../store/navStore";
+
+export const useTOC = () => {
+ const celexId = useNavState((state) => state.celexId);
+ const query = useQuery({
+ queryKey: ["toc", celexId],
+ queryFn: () => getToc(celexId!, Language.ENG),
+ enabled: !!celexId,
+ });
+ return query;
+};
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index faff43a..ec59767 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -1,5 +1,4 @@
-import TOC from "../components/TOC/TOC";
-import { Language } from "./types";
+import { Division, Language } from "./types";
const API_URL = import.meta.env.VITE_API_URL;
@@ -23,10 +22,13 @@ async function getArticleIds(celexId: string): Promise
{
return await response.json();
}
-async function getToc(celexId: string, language: Language): Promise {
+async function getToc(
+ celexId: string,
+ language: Language
+): Promise {
console.debug(`Fetching TOC for CELEX ID ${celexId}`);
const response = await fetch(`${API_URL}/${celexId}/toc/${language}`);
- return await response.json();
+ return (await response.json()) as Division[];
}
export { getArticle, getArticleIds, getToc };