From e31ed72957035e41ee49175dac5e013cc37e7218 Mon Sep 17 00:00:00 2001 From: Adrian Rumpold Date: Wed, 21 May 2025 12:29:12 +0200 Subject: [PATCH] Implement citations --- src/App.tsx | 25 ++++-- .../CitationEntry/CitationEntry.css | 16 ++++ .../CitationEntry/CitationEntry.tsx | 60 +++++++++++++ .../GlossaryEntry/GlossaryEntry.css | 10 +++ .../GlossaryEntry/GlossaryEntry.tsx | 15 +++- src/hooks/useGlossary.ts | 26 ++++-- src/index.css | 6 +- src/lib/nist-api.ts | 84 ++++++++++++++++--- src/theme.css | 2 + 9 files changed, 216 insertions(+), 28 deletions(-) create mode 100644 src/components/CitationEntry/CitationEntry.css create mode 100644 src/components/CitationEntry/CitationEntry.tsx diff --git a/src/App.tsx b/src/App.tsx index ea66381..201b6aa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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(null); - if (isPending) return
Loading...
; - if (error) return
Error: {error.message}
; + if (isLoading) return
Loading...
; + if (isError) return
Error while fetching
; - const entries = glossary?.definitions + const entries = glossary! .filter((entry) => { if (!stringFilter) { return true; @@ -34,9 +37,21 @@ function App() { {entries.length} entries found + {selectedCitation && ( + citation.key === selectedCitation)! + } + onClose={() => setSelectedCitation(null)} + /> + )}
{entries.map((term) => ( - + ))}
diff --git a/src/components/CitationEntry/CitationEntry.css b/src/components/CitationEntry/CitationEntry.css new file mode 100644 index 0000000..38b6823 --- /dev/null +++ b/src/components/CitationEntry/CitationEntry.css @@ -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; + } +} diff --git a/src/components/CitationEntry/CitationEntry.tsx b/src/components/CitationEntry/CitationEntry.tsx new file mode 100644 index 0000000..178f748 --- /dev/null +++ b/src/components/CitationEntry/CitationEntry.tsx @@ -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(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 ( +
+
+
+ {citation.url ? ( + + {richTitle} + + ) : ( + richTitle + )} +
+ {citation.authors &&
{citation.authors}
} + {citation.publication &&
{citation.publication}
} + {citation.issue &&
Issue {citation.issue}
} + {citation.volume &&
Volume {citation.volume}
} + {citation.pages &&
{citation.pages}
} +
+
+ ); +} diff --git a/src/components/GlossaryEntry/GlossaryEntry.css b/src/components/GlossaryEntry/GlossaryEntry.css index 5af8f7c..ca63fff 100644 --- a/src/components/GlossaryEntry/GlossaryEntry.css +++ b/src/components/GlossaryEntry/GlossaryEntry.css @@ -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: " ↗"; +} diff --git a/src/components/GlossaryEntry/GlossaryEntry.tsx b/src/components/GlossaryEntry/GlossaryEntry.tsx index d0c562e..85eda12 100644 --- a/src/components/GlossaryEntry/GlossaryEntry.tsx +++ b/src/components/GlossaryEntry/GlossaryEntry.tsx @@ -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 ( <>
{term.term}
{term.definitions.map((def, index) => ( -
{def.definition}
+
+ {def.definition} + + onSelectCitation && onSelectCitation(def.citationKey) + } + > + {def.citationKey} + +
))} ); diff --git a/src/hooks/useGlossary.ts b/src/hooks/useGlossary.ts index 37e78fd..54e416b 100644 --- a/src/hooks/useGlossary.ts +++ b/src/hooks/useGlossary.ts @@ -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; diff --git a/src/index.css b/src/index.css index 5d9316e..f99fd3e 100644 --- a/src/index.css +++ b/src/index.css @@ -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 { diff --git a/src/lib/nist-api.ts b/src/lib/nist-api.ts index f1847b3..26a71be 100644 --- a/src/lib/nist-api.ts +++ b/src/lib/nist-api.ts @@ -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(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(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(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; }; diff --git a/src/theme.css b/src/theme.css index a794de7..57b4973 100644 --- a/src/theme.css +++ b/src/theme.css @@ -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;