diff --git a/app/api/model-labels/route.ts b/app/api/model-labels/route.ts index 0a1cd776..415b8d53 100644 --- a/app/api/model-labels/route.ts +++ b/app/api/model-labels/route.ts @@ -46,7 +46,18 @@ export async function PUT(request: NextRequest) { const config: ModelLabelsConfig = { labels: normalizeModelLabels((body as { labels?: unknown }).labels as Record), }; - await writeModelLabels(config); + try { + await writeModelLabels(config, token); + } catch (err) { + const status = + err && + typeof err === 'object' && + 'status' in err && + typeof (err as { status: unknown }).status === 'number' + ? (err as { status: number }).status + : 502; + return NextResponse.json({ error: 'Failed to update site labels' }, { status }); + } revalidatePath('/', 'layout'); return NextResponse.json(config); } diff --git a/app/backoffice/translations/page.tsx b/app/backoffice/translations/page.tsx index 47ea9613..e01dca62 100644 --- a/app/backoffice/translations/page.tsx +++ b/app/backoffice/translations/page.tsx @@ -31,14 +31,19 @@ const generalConfigFieldMeta: Array<{ key: ModelLabelKey; title: string; descrip description: 'The short strapline shown next to the site title in the header.', }, { - key: 'footerFunded', - title: 'Footer: Funding Statement', - description: 'The funding acknowledgement shown in the footer.', + key: 'footerLine1', + title: 'Footer: Line 1', + description: 'The first line of footer text.', }, { - key: 'footerCopyright', - title: 'Footer: Copyright Notice', - description: 'The copyright notice shown at the bottom of the footer.', + key: 'footerLine2', + title: 'Footer: Line 2', + description: 'The second line of footer text.', + }, + { + key: 'footerBottomLine', + title: 'Footer: Bottom Line', + description: 'The copyright/attribution notice shown at the bottom of the footer.', }, ]; diff --git a/components/layout/footer.tsx b/components/layout/footer.tsx index 70f98b80..58cd3613 100644 --- a/components/layout/footer.tsx +++ b/components/layout/footer.tsx @@ -57,7 +57,7 @@ export default async function Footer() { getPublishedPages(), ]); const locale = rawLocale as ModelLabelLocale; - const getLabel = (key: 'siteTitle' | 'footerFunded' | 'footerCopyright') => + const getLabel = (key: 'siteTitle' | 'footerLine1' | 'footerLine2' | 'footerBottomLine') => resolveModelLabel(modelLabels.labels[key], locale); const quickLinkPages = pages .filter((page) => page.include_in_quick_link) @@ -76,8 +76,8 @@ export default async function Footer() {

{getLabel('siteTitle')}

-

{t('about')}

-

{getLabel('footerFunded')}

+

{getLabel('footerLine1')}

+

{getLabel('footerLine2')}

{/* Links column */} @@ -131,7 +131,7 @@ export default async function Footer() { {/* Bottom bar */}

- {getLabel('footerCopyright')} + {getLabel('footerBottomLine')}

{ const defaults = getDefaultModelLabelsConfig(); - return readJsonConfig( - CONFIG_FILE, - (raw) => { - // If the file was hand-edited to `null`, an array, or a primitive, - // reading `parsed.labels` below would crash; bail to defaults. - if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return defaults; - const parsed = raw as Partial; - return { labels: normalizeModelLabels(parsed.labels) }; - }, - defaults - ); + try { + const res = await apiFetch(SITE_LABELS_PATH); + if (!res.ok) return defaults; + const raw = await res.json(); + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return defaults; + const parsed = raw as Partial; + return { labels: normalizeModelLabels(parsed.labels) }; + } catch { + return defaults; + } } -export async function writeModelLabels(config: ModelLabelsConfig): Promise { - await writeJsonConfig(CONFIG_FILE, { labels: normalizeModelLabels(config.labels) }); +/** Upserts the given keys via the backend's superuser-only PUT; throws on failure. */ +export async function writeModelLabels(config: ModelLabelsConfig, token: string): Promise { + const res = await authFetch(SITE_LABELS_PATH, token, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ labels: normalizeModelLabels(config.labels) }), + }); + if (!res.ok) { + const details = await res.text().catch(() => ''); + throw Object.assign( + new Error(`Failed to write site labels: ${res.status}${details ? ` - ${details}` : ''}`), + { status: res.status } + ); + } } diff --git a/lib/model-labels.ts b/lib/model-labels.ts index 774279e1..14b0fcc1 100644 --- a/lib/model-labels.ts +++ b/lib/model-labels.ts @@ -22,8 +22,9 @@ export type ModelLabelKey = // General site branding, shown in the header and footer. | 'siteTitle' | 'siteTagline' - | 'footerFunded' - | 'footerCopyright'; + | 'footerLine1' + | 'footerLine2' + | 'footerBottomLine'; export type ModelLabelLocale = 'en' | 'fr'; @@ -59,13 +60,23 @@ export const DEFAULT_MODEL_LABELS: Record = { en: 'Archetype tagline', fr: 'Archetype tagline', }, - footerFunded: { - en: 'Archetype funding text.', - fr: "Texte de financement d'Archetype.", + footerLine1: { + en: 'Footer first section', + fr: 'Pied de page, première section', }, - footerCopyright: { - en: 'Archetype copyright', - fr: "Droits d'auteur d'Archetype", + footerLine2: { + en: 'Footer second section', + fr: 'Pied de page, deuxième section', + }, + footerBottomLine: { + en: + 'Archetype is freely-available software for structured annotations of images which allows users to search for, ' + + 'view, and organise detailed characteristics of handwriting or other material in both verbal and visual form. ' + + 'Archetype is designed primarily for the palaeographical analysis of handwriting.', + fr: + 'Archetype est un logiciel libre pour les annotations structurées d’images qui permet aux utilisateurs de rechercher' + + ' , de visualiser et d’organiser les caractéristiques détaillées de l’écriture ou d’autres matériaux sous forme verbale et visuelle. ' + + 'Archetype est conçu principalement pour l’analyse paléographique de l’écriture.', }, };