diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ccca8ea..f385a44 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,7 +11,8 @@ "@tanstack/react-query": "^5.74.4", "@tanstack/react-query-devtools": "^5.74.6", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-router-dom": "^7.5.1" }, "devDependencies": { "@eslint/js": "^9.22.0", @@ -1934,6 +1935,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2905,6 +2915,45 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.1.tgz", + "integrity": "sha512-/jjU3fcYNd2bwz9Q0xt5TwyiyoO8XjSEFXJY4O/lMAlkGTHWuHRAbR9Etik+lSDqMC7A7mz3UlXzgYT6Vl58sA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.1.tgz", + "integrity": "sha512-5DPSPc7ENrt2tlKPq0FtpG80ZbqA9aIKEyqX6hSNJDlol/tr6iqCK4crqdsusmOSSotq6zDsn0y3urX9TuTNmA==", + "license": "MIT", + "dependencies": { + "react-router": "7.5.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3006,6 +3055,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3136,6 +3191,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6f69164..311d9d7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,8 @@ "@tanstack/react-query": "^5.74.4", "@tanstack/react-query-devtools": "^5.74.6", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-router-dom": "^7.5.1" }, "devDependencies": { "@eslint/js": "^9.22.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9af05cd..42fc738 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,16 +2,20 @@ import { useState } from "react"; import Panel from "./components/Panel"; import { useQueries } from "@tanstack/react-query"; +import { useNavigate } from "react-router-dom"; import "./App.css"; import ArticleSelector from "./components/ArticleSelector"; -import CelexSelector from "./components/CelexSelector"; import TOC from "./components/TOC"; import { getArticleIds, getToc } from "./lib/api"; import { Language } from "./lib/types"; -function App() { - const [celexId, setCelexId] = useState("32024R1689"); - const [selectedArticle, setSelectedArticle] = useState(1); +type Props = { + celexId: string; + articleId: number; +}; + +function App({ celexId, articleId }: Props) { + const navigate = useNavigate(); const [numPanels, setNumPanels] = useState(1); const [selectedParagraphId, setSelectedParagraphId] = useState( null @@ -35,10 +39,10 @@ function App() { const error = results.find((result) => result.isError); if (isPending) { - return
Loading...
; + return
Loading...
; } if (error) { - return
Error: {error.error?.message}
; + return
Error: {error.error?.message}
; } const examples = [ @@ -56,9 +60,7 @@ function App() { id="examples" value={celexId} onChange={(e) => { - setSelectedArticle(1); - setSelectedParagraphId(null); - setCelexId(e.currentTarget.value); + navigate(`/${e.target.value}`); }} > {examples.map((example) => ( @@ -68,11 +70,11 @@ function App() { ))} - + {/* */} navigate(`/${celexId}/articles/${id}`)} /> -
    - {toc.map((division, index) => ( -
  • - {division.title} -
    - {division.subtitle} -
      - {division.articles.map((article) => ( -
    • onArticleSelected(article.id)} - > - {article.title}: {article.subtitle} -
    • - ))} -
    -
  • - ))} -
+ {toc.map((division) => renderDivision(division))} ); } diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 74ed8df..04f08f8 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -5,5 +5,19 @@ enum Language { ITA = "ita", ESP = "esp", } +type Article = { + type: "article"; + id: number; + title: string; + subtitle: string; +}; +type Division = { + type: "division"; + title: string; + subtitle: string; + level: number; + content: Article[] | Division[]; +}; export { Language }; +export type { Article, Division }; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index c6be869..6753157 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,7 +2,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import App from "./App.tsx"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import MainView from "./MainView"; + import "./index.css"; const queryClient = new QueryClient(); @@ -11,7 +13,14 @@ createRoot(document.getElementById("root")!).render( - + + + Select a CELEX ID} /> + + } /> + + + ); diff --git a/src/formex_viewer/server.py b/src/formex_viewer/server.py index 8b45c0f..1567aa9 100644 --- a/src/formex_viewer/server.py +++ b/src/formex_viewer/server.py @@ -28,7 +28,7 @@ type CacheKey = tuple[str, Language] CACHE: dict[CacheKey, str] = {} -def _get_fmx4_data(celex_id: str, language: Language) -> str: +def _get_fmx4_data(celex_id: str, language: Language) -> ET.Element: """ Fetch the FMX4 data from the server. """ @@ -43,9 +43,10 @@ def _get_fmx4_data(celex_id: str, language: Language) -> str: ) fmx4_data = client.publication_text(cellar_id, ContentType.ZIP_FMX4) - CACHE[(celex_id, language)] = fmx4_data + xml = ET.fromstring(fmx4_data.encode("utf-8")) + CACHE[(celex_id, language)] = xml - return fmx4_data + return xml @app.get("/{celex_id}/articles") @@ -53,8 +54,7 @@ def article_ids(celex_id: str, language: Language = Language.ENG): """ Fetch the article IDs from the server. """ - fmx4_data = _get_fmx4_data(celex_id, language) - xml = ET.fromstring(fmx4_data.encode("utf-8")) + xml = _get_fmx4_data(celex_id, language) article_xpath = "//ARTICLE/@IDENTIFIER" article_ids = xml.xpath(article_xpath) @@ -65,18 +65,16 @@ def article_ids(celex_id: str, language: Language = Language.ENG): @app.get("/{celex_id}/toc/{language}") def toc(celex_id: str, language: Language = Language.ENG): - """ - Fetch the table of contents from the server. - """ - fmx4_data = _get_fmx4_data(celex_id, language) - xml = ET.fromstring(fmx4_data.encode("utf-8")) - - toc = [] - - for division in xml.xpath("//DIVISION"): + def _handle_division(division: ET.Element, level: int): print(division) + title = ti_el[0] if (ti_el := division.xpath("TITLE/TI//text()")) else "" subtitle = sti_el[0] if (sti_el := division.xpath("TITLE/STI//text()")) else "" + + subdivisions = [] + for subdivision in division.xpath("DIVISION") or []: + subdivisions.append(_handle_division(subdivision, level + 1)) + articles = [] for article in division.xpath("ARTICLE") or []: art_id = article.get("IDENTIFIER") @@ -94,14 +92,23 @@ def toc(celex_id: str, language: Language = Language.ENG): "subtitle": art_subtitle, } ) - toc.append( - { - "title": title, - "type": "division", - "subtitle": subtitle, - "articles": articles, - } - ) + + return { + "type": "division", + "title": title, + "subtitle": subtitle, + "level": level, + "content": subdivisions + articles, + } + + """ + Fetch the table of contents from the server. + """ + xml = _get_fmx4_data(celex_id, language) + toc = [] + + for division in xml.xpath("//ENACTING.TERMS/DIVISION"): + toc.append(_handle_division(division, 0)) return toc @@ -111,8 +118,7 @@ def article(celex_id: str, article_id: int, language: Language = Language.ENG): """ Fetch an article from the server. """ - fmx4_data = _get_fmx4_data(celex_id, language) - xml = ET.fromstring(fmx4_data.encode("utf-8")) + xml = _get_fmx4_data(celex_id, language) article_xpath = "//ARTICLE" articles = xml.xpath(article_xpath)