import { useState, useRef, useEffect } from "react"; import { ChevronUp, ChevronDown, Pencil, Trash2, Plus, ExternalLink, } from "lucide-react"; import { AuthorInlineForm } from "./AuthorInlineForm"; import type { FrontmatterStore, Author, Affiliation } from "./frontmatter-store"; interface AuthorsManagerProps { open: boolean; store: FrontmatterStore; authors: Author[]; affiliations: Affiliation[]; onClose: () => void; } /** * One-stop modal to manage the article byline: add/edit/remove authors, * add/edit/remove affiliations, and reorder both lists with explicit * up/down controls (the discoverable replacement for the invisible inline * drag). All mutations go through the FrontmatterStore so they stay * collaborative + undoable; reordering or deleting an affiliation remaps * every author's 1-based references inside the store. */ export function AuthorsManager({ open, store, authors, affiliations, onClose, }: AuthorsManagerProps) { const dialogRef = useRef(null); // `null` = closed, `"new"` = add form, number = edit author at that index. const [authorForm, setAuthorForm] = useState(null); const [newAffName, setNewAffName] = useState(""); useEffect(() => { const dialog = dialogRef.current; if (!dialog) return; if (open && !dialog.open) { setAuthorForm(null); setNewAffName(""); dialog.showModal(); } else if (!open && dialog.open) { dialog.close(); } }, [open]); useEffect(() => { const dialog = dialogRef.current; if (!dialog) return; const onCloseEvent = () => onClose(); dialog.addEventListener("close", onCloseEvent); return () => dialog.removeEventListener("close", onCloseEvent); }, [onClose]); const multipleAffiliations = affiliations.length > 1; const submitAuthorForm = (author: Author, newAff?: Affiliation) => { const finalAuthor = { ...author }; if (newAff) { const idx = store.addAffiliation(newAff); finalAuthor.affiliations = [...finalAuthor.affiliations, idx]; } if (authorForm === "new") { store.addAuthor(finalAuthor); } else if (typeof authorForm === "number") { store.updateAuthor(authorForm, finalAuthor); } setAuthorForm(null); }; const addAffiliation = () => { const name = newAffName.trim(); if (!name) return; store.addAffiliation({ name }); setNewAffName(""); }; return (

Authors & affiliations

{/* ---- Authors ---- */}
Authors
{authors.length === 0 ? (

No authors yet.

) : (
    {authors.map((author, i) => (
  • {author.name || Unnamed} {multipleAffiliations && author.affiliations?.length > 0 && ( {[...author.affiliations].sort((a, b) => a - b).join(",")} )} {author.url && ( {prettyUrl(author.url)} )}
  • ))}
)}
{/* ---- Affiliations ---- */}
Affiliations
{affiliations.length === 0 ? (

No affiliations yet. Add one below, then attach it to authors.

) : (
    {affiliations.map((aff, i) => ( ))}
)}
setNewAffName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && addAffiliation()} />
{/* Author add/edit form, stacked on top of this dialog. */} setAuthorForm(null)} />
); } /** * Single affiliation row with inline name + URL editing. Keeps a local draft * so typing doesn't round-trip through Yjs on every keystroke; commits on * blur / Enter only when the value actually changed. */ function AffiliationRow({ index, total, affiliation, store, }: { index: number; total: number; affiliation: Affiliation; store: FrontmatterStore; }) { const [name, setName] = useState(affiliation.name); const [url, setUrl] = useState(affiliation.url || ""); useEffect(() => setName(affiliation.name), [affiliation.name]); useEffect(() => setUrl(affiliation.url || ""), [affiliation.url]); const commit = () => { const nextName = name.trim(); const nextUrl = url.trim(); if (nextName === affiliation.name && nextUrl === (affiliation.url || "")) return; if (!nextName) { // Don't allow blanking the name: revert to the stored value. setName(affiliation.name); return; } store.updateAffiliation(index, { name: nextName, url: nextUrl || undefined, }); }; return (
  • {index + 1}.
    setName(e.target.value)} onBlur={commit} onKeyDown={(e) => e.key === "Enter" && (e.target as HTMLInputElement).blur()} /> setUrl(e.target.value)} onBlur={commit} onKeyDown={(e) => e.key === "Enter" && (e.target as HTMLInputElement).blur()} />
  • ); } /** Strip protocol + trailing slash for a compact URL label. */ function prettyUrl(url: string): string { return url.replace(/^https?:\/\//, "").replace(/\/$/, ""); }