Improved article selector with titles and structure
This commit is contained in:
17
frontend/package-lock.json
generated
17
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 <div>Loading...</div>;
|
||||
}
|
||||
if (error) {
|
||||
return <div>Error: {error.error?.message}</div>;
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.App}>
|
||||
<div className={styles.controls}>
|
||||
<CelexSelector />
|
||||
<ArticleSelector articleIds={results[0].data!} />
|
||||
<ArticleSelector toc={toc} />
|
||||
<button onClick={addPanel}>Add Panel</button>
|
||||
</div>
|
||||
<div className={styles.panelContainer}>
|
||||
<TOC toc={results[1].data!} />
|
||||
<TOC toc={toc} />
|
||||
{Array.from({ length: numPanels }, (_, index) => (
|
||||
<Panel
|
||||
key={index}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.articleSelector {
|
||||
width: 40ch;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -1,32 +1,54 @@
|
||||
import { JSX } from "react";
|
||||
import type { Division } from "../../lib/types";
|
||||
import useNavState from "../../store/navStore";
|
||||
import styles from "./ArticleSelector.module.css";
|
||||
|
||||
type ArticleSelectorProps = {
|
||||
articleIds: number[];
|
||||
toc: Division[];
|
||||
};
|
||||
|
||||
function ArticleSelector({ articleIds }: ArticleSelectorProps) {
|
||||
function ArticleSelector({ toc }: ArticleSelectorProps) {
|
||||
const { articleId, setArticleId } = useNavState();
|
||||
|
||||
function renderDivision(div: Division): JSX.Element {
|
||||
const contents = div.content.map((c) => {
|
||||
if (c.type === "division") {
|
||||
return renderDivision(c);
|
||||
} else {
|
||||
return (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.title}: {c.subtitle}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (div.level === 0) {
|
||||
const title = `${div.title}: ${div.subtitle}`;
|
||||
return (
|
||||
// For top-level divisions, we can use optgroup
|
||||
<optgroup key={title} label={title}>
|
||||
{contents}
|
||||
</optgroup>
|
||||
);
|
||||
} else {
|
||||
// HTML does not support nested optgroups, so we need to flatten the structure
|
||||
return <>{contents}</>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{articleId && articleId > 1 && (
|
||||
<button onClick={() => setArticleId(articleId - 1)}>prev</button>
|
||||
)}
|
||||
<select
|
||||
value={articleId || undefined}
|
||||
className={styles.articleSelector}
|
||||
onChange={(e) => {
|
||||
const id = parseInt(e.currentTarget.value);
|
||||
const id = parseInt(e.target.value);
|
||||
setArticleId(id);
|
||||
}}
|
||||
>
|
||||
{articleIds.map((id) => (
|
||||
<option key={id} value={id}>
|
||||
Article {id}
|
||||
</option>
|
||||
))}
|
||||
{toc.map((div) => renderDivision(div))}
|
||||
</select>
|
||||
{articleId && articleId < articleIds[articleIds.length - 1] && (
|
||||
<button onClick={() => setArticleId(articleId + 1)}>next</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
<div
|
||||
ref={articleRef}
|
||||
lang={lang.substring(0, 2)}
|
||||
dangerouslySetInnerHTML={{ __html: data || "" }}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(data) || "" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
14
frontend/src/hooks/toc.ts
Normal file
14
frontend/src/hooks/toc.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -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<number[]> {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async function getToc(celexId: string, language: Language): Promise<TOC> {
|
||||
async function getToc(
|
||||
celexId: string,
|
||||
language: Language
|
||||
): Promise<Division[]> {
|
||||
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 };
|
||||
|
||||
Reference in New Issue
Block a user