Working TOC
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Panel from "./components/Panel";
|
import Panel from "./components/Panel";
|
||||||
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQueries } from "@tanstack/react-query";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import ArticleSelector from "./components/ArticleSelector";
|
import ArticleSelector from "./components/ArticleSelector";
|
||||||
import CelexSelector from "./components/CelexSelector";
|
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";
|
import { Language } from "./lib/types";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -16,21 +17,28 @@ function App() {
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const results = useQueries({
|
||||||
data: articleIds,
|
queries: [
|
||||||
isPending,
|
{
|
||||||
error,
|
queryKey: ["articleIds", celexId],
|
||||||
} = useQuery({
|
queryFn: () => getArticleIds(celexId),
|
||||||
queryKey: ["articleIds", celexId],
|
enabled: !!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) {
|
if (isPending) {
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
return <div>Error: {error.message}</div>;
|
return <div>Error: {error.error?.message}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const examples = [
|
const examples = [
|
||||||
@@ -62,7 +70,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<CelexSelector defaultId={celexId} onSelected={setCelexId} />
|
<CelexSelector defaultId={celexId} onSelected={setCelexId} />
|
||||||
<ArticleSelector
|
<ArticleSelector
|
||||||
articleIds={articleIds}
|
articleIds={results[0].data!}
|
||||||
selectedId={selectedArticle}
|
selectedId={selectedArticle}
|
||||||
onSelected={setSelectedArticle}
|
onSelected={setSelectedArticle}
|
||||||
/>
|
/>
|
||||||
@@ -71,6 +79,11 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-container">
|
<div className="panel-container">
|
||||||
|
<TOC
|
||||||
|
toc={results[1].data!}
|
||||||
|
selectedArticleId={selectedArticle}
|
||||||
|
onArticleSelected={setSelectedArticle}
|
||||||
|
/>
|
||||||
{Array.from({ length: numPanels }, (_, index) => (
|
{Array.from({ length: numPanels }, (_, index) => (
|
||||||
<Panel
|
<Panel
|
||||||
key={index}
|
key={index}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.panel {
|
.panel {
|
||||||
flex-grow: 1;
|
flex: 1 auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
|
|||||||
54
frontend/src/components/TOC.css
Normal file
54
frontend/src/components/TOC.css
Normal 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;
|
||||||
|
}
|
||||||
62
frontend/src/components/TOC.tsx
Normal file
62
frontend/src/components/TOC.tsx
Normal 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;
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import TOC from "../components/TOC";
|
||||||
|
import { Language } from "./types";
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL;
|
const API_URL = import.meta.env.VITE_API_URL;
|
||||||
|
|
||||||
async function getArticle(
|
async function getArticle(
|
||||||
@@ -20,4 +23,10 @@ async function getArticleIds(celexId: string): Promise<number[]> {
|
|||||||
return await response.json();
|
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 };
|
||||||
|
|||||||
@@ -63,6 +63,49 @@ def article_ids(celex_id: str, language: Language = Language.ENG):
|
|||||||
return article_ids
|
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}")
|
@app.get("/{celex_id}/articles/{article_id}/{language}")
|
||||||
def article(celex_id: str, article_id: int, language: Language = Language.ENG):
|
def article(celex_id: str, article_id: int, language: Language = Language.ENG):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user