carbon-tokenization / frontend /src /editor /EditorFooter.tsx
tfrere's picture
tfrere HF Staff
fix(editor): footer credit links to the editor Space too
2bed071
Raw
History Blame Contribute Delete
5.63 kB
import { useEffect, useState, useCallback, useRef } from "react";
import type { Editor as TiptapEditor } from "@tiptap/core";
import type { FrontmatterStore, FrontmatterData } from "./frontmatter/frontmatter-store";
function extractYear(dateStr?: string): number | undefined {
if (!dateStr) return undefined;
const d = new Date(dateStr);
if (!Number.isNaN(d.getTime())) return d.getFullYear();
const m = dateStr.match(/(19|20)\d{2}/);
return m ? Number(m[0]) : undefined;
}
function buildCitationText(data: FrontmatterData): string {
const names = data.authors.map((a) => a.name).filter(Boolean);
const year = extractYear(data.published);
const title = (data.title || "Untitled").replace(/\s+/g, " ").trim();
return `${names.join(", ")}${year ? ` (${year})` : ""}. "${title}".`;
}
function buildBibtex(data: FrontmatterData): string {
const names = data.authors.map((a) => a.name).filter(Boolean);
const title = (data.title || "Untitled").replace(/\s+/g, " ").trim();
const year = extractYear(data.published);
const keyAuthor = (names[0] || "article").split(/\s+/).slice(-1)[0].toLowerCase();
const keyTitle = title.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
const bibKey = `${keyAuthor}${year ?? ""}_${keyTitle}`;
const parts = [` title={${title}}`, ` author={${names.join(" and ")}}`];
if (year) parts.push(` year={${year}}`);
if (data.doi) parts.push(` doi={${data.doi}}`);
return `@misc{${bibKey},\n${parts.join(",\n")}\n}`;
}
interface FootnoteItem {
index: number;
content: string;
}
function collectFootnotes(editor: TiptapEditor): FootnoteItem[] {
const items: FootnoteItem[] = [];
let i = 0;
editor.state.doc.descendants((node) => {
if (node.type.name === "footnote") {
i++;
items.push({ index: i, content: (node.attrs.content as string) || "" });
}
});
return items;
}
function getBibliographyHtml(editor: TiptapEditor): string {
let html = "";
editor.state.doc.descendants((node) => {
if (node.type.name === "bibliography" && node.attrs.renderedHtml) {
html = node.attrs.renderedHtml as string;
}
});
return html;
}
interface EditorFooterProps {
store: FrontmatterStore | null;
editor: TiptapEditor | null;
}
export function EditorFooter({ store, editor }: EditorFooterProps) {
const [data, setData] = useState<FrontmatterData | null>(null);
const [biblioHtml, setBiblioHtml] = useState("");
const [footnotes, setFootnotes] = useState<FootnoteItem[]>([]);
useEffect(() => {
if (!store) return;
const sync = () => setData(store.getAll());
sync();
return store.observe(sync);
}, [store]);
const refreshEditorData = useCallback(() => {
if (!editor) return;
setBiblioHtml(getBibliographyHtml(editor));
setFootnotes(collectFootnotes(editor));
}, [editor]);
const footerTimerRef = useRef(0);
useEffect(() => {
if (!editor) return;
refreshEditorData();
const debouncedRefresh = () => {
clearTimeout(footerTimerRef.current);
footerTimerRef.current = window.setTimeout(refreshEditorData, 500);
};
editor.on("update", debouncedRefresh);
return () => {
editor.off("update", debouncedRefresh);
clearTimeout(footerTimerRef.current);
};
}, [editor, refreshEditorData]);
if (!data) return null;
const citation = buildCitationText(data);
const bibtex = buildBibtex(data);
return (
<footer className="footer">
<div className="footer-inner">
<section className="citation-block">
<p className="footer-heading" role="heading" aria-level={2}>Citation</p>
<p>For attribution in academic contexts, please cite this work as</p>
<pre className="citation short">{citation}</pre>
<p>BibTeX citation</p>
<pre className="citation long">{bibtex}</pre>
</section>
{data.doi && (
<section className="doi-block">
<p className="footer-heading" role="heading" aria-level={2}>DOI</p>
<p>
<a href={`https://doi.org/${data.doi}`} target="_blank" rel="noopener noreferrer">
{data.doi}
</a>
</p>
</section>
)}
{data.licence && (
<section className="reuse-block">
<p className="footer-heading" role="heading" aria-level={2}>Reuse</p>
<p dangerouslySetInnerHTML={{ __html: data.licence }} />
</section>
)}
{biblioHtml && (
<section className="references-block">
<p className="footer-heading" role="heading" aria-level={2}>References</p>
<div
className="bibliography-content"
dangerouslySetInnerHTML={{ __html: biblioHtml }}
/>
</section>
)}
{footnotes.length > 0 && (
<section className="references-block">
<p className="footer-heading" role="heading" aria-level={2}>Footnotes</p>
<ol className="footnotes-list">
{footnotes.map((fn) => (
<li key={fn.index} id={`fn-${fn.index}`}>
<p>{fn.content || "Empty footnote"}</p>
</li>
))}
</ol>
</section>
)}
<div className="template-credit">
<p>
Made with ❤️ with{" "}
<a href="https://huggingface.co/spaces/tfrere/research-article-template-editor" target="_blank" rel="noopener noreferrer">
research article template editor
</a>
</p>
</div>
</div>
</footer>
);
}