Implement citations

This commit is contained in:
Adrian Rumpold
2025-05-21 12:29:12 +02:00
parent 7a0355aebf
commit e31ed72957
9 changed files with 216 additions and 28 deletions

View File

@@ -1,18 +1,21 @@
import { useState } from "react";
import AlphabeticalFilter from "./components/AlphabeticalFilter/AlphabeticalFilter";
import CitationEntry from "./components/CitationEntry/CitationEntry";
import GlossaryEntry from "./components/GlossaryEntry/GlossaryEntry";
import StringFilter from "./components/StringFilter/StringFilter";
import useGlossary from "./hooks/useGlossary";
function App() {
const { data: glossary, error, isPending } = useGlossary();
const { glossary, citations, isLoading, isError } = useGlossary();
const [alphabeticalFilter, setAlphabeticalFilter] = useState("");
const [stringFilter, setStringFilter] = useState("");
const [selectedCitation, setSelectedCitation] = useState<string | null>(null);
if (isPending) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error while fetching</div>;
const entries = glossary?.definitions
const entries = glossary!
.filter((entry) => {
if (!stringFilter) {
return true;
@@ -34,9 +37,21 @@ function App() {
<StringFilter onChange={setStringFilter} />
<AlphabeticalFilter onChange={setAlphabeticalFilter} />
{entries.length} entries found
{selectedCitation && (
<CitationEntry
citation={
citations!.find((citation) => citation.key === selectedCitation)!
}
onClose={() => setSelectedCitation(null)}
/>
)}
<dl>
{entries.map((term) => (
<GlossaryEntry term={term} key={term.term} />
<GlossaryEntry
term={term}
key={term.term}
onSelectCitation={setSelectedCitation}
/>
))}
</dl>
</div>

View File

@@ -0,0 +1,16 @@
.citation-entry {
display: flex;
flex-direction: column;
padding: 10px;
margin-bottom: 10px;
gap: 1rem;
max-width: 48ch;
.title {
font-weight: bold;
}
.authors {
font-style: italic;
}
}

View File

@@ -0,0 +1,60 @@
import { useEffect, useRef } from "react";
import type { Citation } from "../../lib/nist-api";
import "./CitationEntry.css";
type CitationEntryProps = {
citation?: Citation;
onClose?: () => void;
};
export default function CitationEntry({
citation,
onClose,
}: CitationEntryProps) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) {
return;
}
if (citation) {
ref.current.showPopover();
}
}, [citation]);
if (!citation) {
return null;
}
const richTitle = citation.year
? `${citation.title} (${citation.year})`
: citation.title;
const onToggle = (e: React.ToggleEvent) => {
if (e.newState === "closed") {
onClose?.();
}
};
return (
<div
popover="auto"
ref={ref}
className="citation-modal"
onToggle={onToggle}
>
<div className="citation-entry">
<div className="title">
{citation.url ? (
<a href={citation.url} target="_blank">
{richTitle}
</a>
) : (
richTitle
)}
</div>
{citation.authors && <div className="authors">{citation.authors}</div>}
{citation.publication && <div>{citation.publication}</div>}
{citation.issue && <div>Issue {citation.issue}</div>}
{citation.volume && <div>Volume {citation.volume}</div>}
{citation.pages && <div>{citation.pages}</div>}
</div>
</div>
);
}

View File

@@ -19,3 +19,13 @@ dd + dt {
dt + dd {
margin-top: 1rem;
}
.citation {
display: block;
cursor: pointer;
font-size: 0.8em;
color: var(--color-text-alt);
}
.citation::after {
content: " ↗";
}

View File

@@ -3,14 +3,25 @@ import "./GlossaryEntry.css";
type GlossaryEntryProps = {
term: GlossaryTerm;
onSelectCitation?: (citationKey: string) => void;
};
function GlossaryEntry({ term }: GlossaryEntryProps) {
function GlossaryEntry({ term, onSelectCitation }: GlossaryEntryProps) {
return (
<>
<dt key={term.term}>{term.term}</dt>
{term.definitions.map((def, index) => (
<dd key={term.term + index}>{def.definition}</dd>
<dd key={term.term + index}>
{def.definition}
<a
className="citation"
onClick={() =>
onSelectCitation && onSelectCitation(def.citationKey)
}
>
{def.citationKey}
</a>
</dd>
))}
</>
);

View File

@@ -1,10 +1,24 @@
import { useQuery } from "@tanstack/react-query";
import { fetchGlossary } from "../lib/nist-api";
import { fetchCitations, fetchDefinitions } from "../lib/nist-api";
const useGlossary = () =>
useQuery({
queryKey: ["glossary"],
queryFn: fetchGlossary,
staleTime: 5 * 60 * 1000, // 5 minutes
const useGlossary = () => {
const definitionsQuery = useQuery({
queryKey: ["definitions"],
queryFn: fetchDefinitions,
staleTime: 1000 * 60 * 60, // 1 hour
});
const citationsQuery = useQuery({
queryKey: ["citations"],
queryFn: fetchCitations,
staleTime: 1000 * 60 * 60, // 1 hour
});
return {
glossary: definitionsQuery.data,
citations: citationsQuery.data,
isLoading: definitionsQuery.isLoading || citationsQuery.isLoading,
isError: definitionsQuery.isError || citationsQuery.isError,
};
};
export default useGlossary;

View File

@@ -25,7 +25,7 @@ a:hover {
}
body {
margin: 0;
margin: 1rem;
display: flex;
flex-direction: column;
place-items: center;
@@ -34,8 +34,7 @@ body {
}
footer {
padding: 1rem;
margin: 0 2rem;
margin-top: 2rem;
font-size: 0.6rem;
}
@@ -44,7 +43,6 @@ footer {
flex-direction: column;
place-items: center;
width: 100%;
padding: 2rem 0;
}
button {

View File

@@ -30,23 +30,58 @@ export type Glossary = {
citations: Citation[];
};
const glossaryUrl =
const baseUrl =
"https://docs.google.com/spreadsheets/d/e/2PACX-1vTRBYglcOtgaMrdF11aFxfEY3EmB31zslYI4q2_7ZZ8z_1lKm7OHtF0t4xIsckuogNZ3hRZAaDQuv_K/pub?output=csv";
const glossaryGid = "0";
const citationGid = "2053825396";
export const fetchGlossary = async () => {
const resp = await new Axios().get(glossaryUrl);
const data = resp.data;
return parseGlossary(data);
const makeClient = () => {
const client = new Axios({
baseURL: baseUrl,
});
return client;
};
const parseGlossary = (data: string): Glossary => {
const parsed = Papa.parse<string[]>(data, {
export const fetchCitations = async () => {
const client = makeClient();
const resp = await client.get(baseUrl, {
params: {
gid: citationGid,
},
});
return parseCitations(resp.data);
};
export const fetchDefinitions = async () => {
const client = makeClient();
const resp = await client.get(baseUrl, {
params: {
gid: glossaryGid,
},
});
return parseDefinitions(resp.data);
};
export const parseGlossary = (
glossaryData: string,
citationData: string
): Glossary => {
const glossary: Glossary = {
definitions: parseDefinitions(glossaryData),
citations: parseCitations(citationData),
};
return glossary;
};
const parseDefinitions = (glossaryData: string): GlossaryTerm[] => {
const terms: GlossaryTerm[] = [];
const parsed = Papa.parse<string[]>(glossaryData, {
delimiter: ",",
header: false,
dynamicTyping: true,
skipEmptyLines: true,
});
const glossary: Glossary = { definitions: [], citations: [] };
for (const record of parsed.data.slice(1)) {
const fields = record.map((field) =>
@@ -70,7 +105,7 @@ const parseGlossary = (data: string): Glossary => {
const legalDefinition = fields[fields.length - 1];
if (term && definitions.length > 0) {
glossary.definitions.push({
terms.push({
term,
definitions,
relatedTerms,
@@ -78,6 +113,33 @@ const parseGlossary = (data: string): Glossary => {
});
}
}
return glossary;
return terms;
};
const parseCitations = (citationData: string): Citation[] => {
const citations: Citation[] = [];
const parsed = Papa.parse<string[]>(citationData, {
delimiter: ",",
header: false,
dynamicTyping: true,
skipEmptyLines: true,
});
for (const record of parsed.data.slice(1)) {
const fields = record;
const citation: Citation = {
key: fields[0],
title: fields[1],
authors: fields[2],
publication: fields[3],
volume: fields[4],
issue: fields[5],
pages: fields[6],
year: fields[7],
url: fields[8],
};
citations.push(citation);
}
return citations;
};

View File

@@ -4,6 +4,7 @@
--color-background: #242424;
--color-background-alt: #1e1e1e;
--color-text: #ffffff;
--color-text-alt: #aaaaaa;
--color-accent: #646cff;
--color-accent-hover: #535bf2;
--color-border: #444444;
@@ -14,6 +15,7 @@
--color-background: #ffffff;
--color-background-alt: #f5f5f5;
--color-text: #213547;
--color-text-alt: #000000;
--color-accent: #747bff;
--color-accent-hover: #646cff;
--color-border: #d9d9d9;