Working TOC

This commit is contained in:
Adrian Rumpold
2025-04-23 12:11:53 +02:00
parent 54a3aba531
commit 6dcf39dc58
6 changed files with 195 additions and 14 deletions

View File

@@ -1,11 +1,12 @@
import { useState } from "react";
import Panel from "./components/Panel";
import { useQuery } from "@tanstack/react-query";
import { useQueries } from "@tanstack/react-query";
import "./App.css";
import ArticleSelector from "./components/ArticleSelector";
import CelexSelector from "./components/CelexSelector";
import { getArticleIds } from "./lib/api";
import TOC from "./components/TOC";
import { getArticleIds, getToc } from "./lib/api";
import { Language } from "./lib/types";
function App() {
@@ -16,21 +17,28 @@ function App() {
null
);
const {
data: articleIds,
isPending,
error,
} = useQuery({
queryKey: ["articleIds", celexId],
queryFn: () => getArticleIds(celexId),
enabled: !!celexId,
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);
if (isPending) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
return <div>Error: {error.error?.message}</div>;
}
const examples = [
@@ -62,7 +70,7 @@ function App() {
</div>
<CelexSelector defaultId={celexId} onSelected={setCelexId} />
<ArticleSelector
articleIds={articleIds}
articleIds={results[0].data!}
selectedId={selectedArticle}
onSelected={setSelectedArticle}
/>
@@ -71,6 +79,11 @@ function App() {
</button>
</div>
<div className="panel-container">
<TOC
toc={results[1].data!}
selectedArticleId={selectedArticle}
onArticleSelected={setSelectedArticle}
/>
{Array.from({ length: numPanels }, (_, index) => (
<Panel
key={index}

View File

@@ -1,5 +1,5 @@
.panel {
flex-grow: 1;
flex: 1 auto;
padding: 1rem;
border-radius: 8px;
border: 1px solid #ccc;

View File

@@ -0,0 +1,54 @@
.toc {
font-size: 0.8rem;
min-width: 25vw;
flex: 1 auto;
&.hidden {
flex: 0 0;
min-width: 0;
}
transition: width 0.3s ease-in-out;
overflow: scroll;
max-height: 100vh;
.division-list {
list-style: none;
ul {
margin-block: 1rem;
}
}
.selected {
font-weight: bold;
}
.article {
cursor: pointer;
}
}
.toggle-button {
position: fixed;
top: 16px;
left: 16px;
width: 32px;
height: 32px;
padding: 0;
border: none;
border-radius: 50%;
background-color: #007bff;
color: white;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 1000;
}
.toggle-button:hover {
background-color: #0056b3;
}

View File

@@ -0,0 +1,62 @@
import { useState } from "react";
import "./TOC.css";
type Article = {
type: "article";
id: number;
title: string;
subtitle: string;
};
type Division = {
type: "division";
title: string;
subtitle: string;
articles: Article[];
};
type TOC = Division[];
type TOCProps = {
toc: TOC;
selectedArticleId?: number;
onArticleSelected: (articleId: number) => void;
};
function TOC({ toc, selectedArticleId, onArticleSelected }: TOCProps) {
const [isVisible, setIsVisible] = useState(true);
return (
<nav className={`toc ${isVisible ? "" : "hidden"}`}>
<button
onClick={() => setIsVisible(!isVisible)}
className="toggle-button"
>
{isVisible ? "<" : ">"}
</button>
<ul className="division-list">
{toc.map((division, index) => (
<li key={index}>
<strong>{division.title}</strong>
<br />
{division.subtitle}
<ul>
{division.articles.map((article) => (
<li
key={article.id}
className={[
"article",
selectedArticleId === article.id ? "selected" : "",
].join(" ")}
onClick={() => onArticleSelected(article.id)}
>
{article.title}: <em>{article.subtitle}</em>
</li>
))}
</ul>
</li>
))}
</ul>
</nav>
);
}
export default TOC;

View File

@@ -1,3 +1,6 @@
import TOC from "../components/TOC";
import { Language } from "./types";
const API_URL = import.meta.env.VITE_API_URL;
async function getArticle(
@@ -20,4 +23,10 @@ async function getArticleIds(celexId: string): Promise<number[]> {
return await response.json();
}
export { getArticle, getArticleIds };
async function getToc(celexId: string, language: Language): Promise<TOC> {
console.debug(`Fetching TOC for CELEX ID ${celexId}`);
const response = await fetch(`${API_URL}/${celexId}/toc/${language}`);
return await response.json();
}
export { getArticle, getArticleIds, getToc };

View File

@@ -63,6 +63,49 @@ def article_ids(celex_id: str, language: Language = Language.ENG):
return article_ids
@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"):
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 ""
articles = []
for article in division.xpath("ARTICLE") or []:
art_id = article.get("IDENTIFIER")
if not art_id:
continue
art_title = ti_el[0] if (ti_el := article.xpath("TI.ART//text()")) else ""
art_subtitle = (
sti_el[0] if (sti_el := article.xpath("STI.ART//text()")) else ""
)
articles.append(
{
"id": int(art_id.lstrip("0")),
"type": "article",
"title": art_title,
"subtitle": art_subtitle,
}
)
toc.append(
{
"title": title,
"type": "division",
"subtitle": subtitle,
"articles": articles,
}
)
return toc
@app.get("/{celex_id}/articles/{article_id}/{language}")
def article(celex_id: str, article_id: int, language: Language = Language.ENG):
"""