Working TOC
This commit is contained in:
@@ -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({
|
||||
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}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.panel {
|
||||
flex-grow: 1;
|
||||
flex: 1 auto;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
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;
|
||||
|
||||
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 };
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user