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 + + {contents} + + ); + } 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 };